diff --git a/_examples/file-server/embedding-files-into-app/main_test.go b/_examples/file-server/embedding-files-into-app/main_test.go
index 3d74f175..10aaf0ec 100644
--- a/_examples/file-server/embedding-files-into-app/main_test.go
+++ b/_examples/file-server/embedding-files-into-app/main_test.go
@@ -34,9 +34,9 @@ func (r resource) loadFromBase(dir string) string {
}
result := string(b)
- //if runtime.GOOS != "windows" {
- // result = strings.Replace(result, "\n", "\r\n", -1)
- //}
+ if runtime.GOOS != "windows" {
+ result = strings.Replace(result, "\n", "\r\n", -1)
+ }
return result
}
diff --git a/_examples/file-server/single-page-application/basic/main_test.go b/_examples/file-server/single-page-application/basic/main_test.go
index d0a383b6..922356a0 100644
--- a/_examples/file-server/single-page-application/basic/main_test.go
+++ b/_examples/file-server/single-page-application/basic/main_test.go
@@ -3,6 +3,7 @@ package main
import (
"io/ioutil"
"path/filepath"
+ "runtime"
"strings"
"testing"
@@ -35,6 +36,7 @@ func (r resource) loadFromBase(dir string) string {
}
result := string(b)
+
return result
}
diff --git a/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go b/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go
index 77b6fe48..2fe89b40 100644
--- a/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go
+++ b/_examples/file-server/single-page-application/embedded-single-page-application/main_test.go
@@ -3,6 +3,7 @@ package main
import (
"io/ioutil"
"path/filepath"
+ "runtime"
"strings"
"testing"
@@ -34,9 +35,9 @@ func (r resource) loadFromBase(dir string) string {
panic(fullpath + " failed with error: " + err.Error())
}
result := string(b)
- // if runtime.GOOS != "windows" {
- // result = strings.Replace(result, "\n", "\r\n", -1)
- // }
+ if runtime.GOOS != "windows" {
+ result = strings.Replace(result, "\n", "\r\n", -1)
+ }
return result
}
diff --git a/_examples/mvc/cache/main.go b/_examples/mvc/cache/main.go
new file mode 100644
index 00000000..56a61b6b
--- /dev/null
+++ b/_examples/mvc/cache/main.go
@@ -0,0 +1,105 @@
+/*
+If you want to use it as middleware for the entire controller
+you can use its router which is just a sub router to add it as you normally do with standard API:
+
+I'll show you 4 different methods for adding a middleware into an mvc application,
+all of those 4 do exactly the same thing, select what you prefer,
+I prefer the last code-snippet when I need the middleware to be registered somewhere
+else as well, otherwise I am going with the first one:
+
+```go
+// 1
+mvc.Configure(app.Party("/user"), func(m *mvc.Application) {
+ m.Router.Use(cache.Handler(10*time.Second))
+})
+```
+
+```go
+// 2
+// same:
+userRouter := app.Party("/user")
+userRouter.Use(cache.Handler(10*time.Second))
+mvc.Configure(userRouter, ...)
+```
+
+```go
+// 3
+// same:
+userRouter := app.Party("/user", cache.Handler(10*time.Second))
+mvc.Configure(userRouter, ...)
+```
+
+```go
+// 4
+// same:
+app.PartyFunc("/user", func(r iris.Party){
+ r.Use(cache.Handler(10*time.Second))
+ mvc.Configure(r, ...)
+})
+```
+
+If you want to use a middleware for a single route,
+for a single controller's method that is already registered by the engine
+and not by custom `Handle` (which you can add
+the middleware there on the last parameter) and it's not depend on the `Next Handler` to do its job
+then you just call it on the method:
+
+```go
+var myMiddleware := myMiddleware.New(...) // this should return an iris/context.Handler
+
+type UserController struct{}
+func (c *UserController) GetSomething(ctx iris.Context) {
+ // ctx.Proceed checks if myMiddleware called `ctx.Next()`
+ // inside it and returns true if so, otherwise false.
+ nextCalled := ctx.Proceed(myMiddleware)
+ if !nextCalled {
+ return
+ }
+
+ // else do the job here, it's allowed
+}
+```
+
+And last, if you want to add a middleware on a specific method
+and it depends on the next and the whole chain then you have to do it
+using the `AfterActivation` like the example below:
+*/
+package main
+
+import (
+ "time"
+
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/cache"
+ "github.com/kataras/iris/mvc"
+)
+
+var cacheHandler = cache.Handler(10 * time.Second)
+
+func main() {
+ app := iris.New()
+ // You don't have to use .Configure if you do it all in the main func
+ // mvc.Configure and mvc.New(...).Configure() are just helpers to split
+ // your code better, here we use the simplest form:
+ m := mvc.New(app)
+ m.Handle(&exampleController{})
+
+ app.Run(iris.Addr(":8080"))
+}
+
+type exampleController struct{}
+
+func (c *exampleController) AfterActivation(a mvc.AfterActivation) {
+ // select the route based on the method name you want to
+ // modify.
+ index := a.GetRoute("Get")
+ // just prepend the handler(s) as middleware(s) you want to use.
+ // or append for "done" handlers.
+ index.Handlers = append([]iris.Handler{cacheHandler}, index.Handlers...)
+}
+
+func (c *exampleController) Get() string {
+ // refresh every 10 seconds and you will see different time output.
+ now := time.Now().Format("Mon, Jan 02 2006 15:04:05")
+ return "last time executed without cache: " + now
+}
diff --git a/cache/LICENSE b/cache/LICENSE
index 52f7b63e..6beda715 100644
--- a/cache/LICENSE
+++ b/cache/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2017 The Iris Cache Authors. All rights reserved.
+Copyright (c) 2017-2018 The Iris Cache Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
diff --git a/cache/cache.go b/cache/cache.go
index 1f129b4a..09d5ab91 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -7,18 +7,17 @@ Example code:
"time"
"github.com/kataras/iris"
- "github.com/kataras/iris/context"
"github.com/kataras/iris/cache"
)
func main(){
app := iris.Default()
- cachedHandler := cache.WrapHandler(h, 2 *time.Minute)
- app.Get("/hello", cachedHandler)
+ middleware := cache.Handler(2 *time.Minute)
+ app.Get("/hello", middleware, h)
app.Run(iris.Addr(":8080"))
}
- func h(ctx context.Context) {
+ func h(ctx iris.Context) {
ctx.HTML("
Hello, this should be cached. Every 2 minutes it will be refreshed, check your browser's inspector
")
}
*/
@@ -32,46 +31,29 @@ import (
"github.com/kataras/iris/context"
)
-// Cache accepts two parameters
-// first is the context.Handler which you want to cache its result
-// the second is, optional, the cache Entry's expiration duration
+// Cache accepts the cache expiration duration
// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler, which you can use as your default router or per-route handler
//
// All types of response can be cached, templates, json, text, anything.
//
// You can add validators with this function.
-func Cache(bodyHandler context.Handler, expiration time.Duration) *client.Handler {
- return client.NewHandler(bodyHandler, expiration)
-}
-
-// WrapHandler accepts two parameters
-// first is the context.Handler which you want to cache its result
-// the second is, optional, the cache Entry's expiration duration
-// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
-// returns context.Handler, which you can use as your default router or per-route handler
-//
-// All types of response can be cached, templates, json, text, anything.
-//
-// it returns a context.Handler, for more options use the `Cache`
-func WrapHandler(bodyHandler context.Handler, expiration time.Duration) context.Handler {
- return Cache(bodyHandler, expiration).ServeHTTP
+func Cache(expiration time.Duration) *client.Handler {
+ return client.NewHandler(expiration)
}
// Handler accepts one single parameter:
-// the cache Entry's expiration duration
+// the cache expiration duration
// if the expiration <=2 seconds then expiration is taken by the "cache-control's maxage" header
// returns context.Handler.
//
-// It's the same as Cache and WrapHandler but it sets the "bodyHandler" to the next handler in the chain.
-//
// All types of response can be cached, templates, json, text, anything.
//
// it returns a context.Handler which can be used as a middleware, for more options use the `Cache`.
//
// Examples can be found at: https://github.com/kataras/iris/tree/master/_examples/#caching
func Handler(expiration time.Duration) context.Handler {
- h := WrapHandler(nil, expiration)
+ h := Cache(expiration).ServeHTTP
return h
}
diff --git a/cache/cache_test.go b/cache/cache_test.go
index 12d448c6..8c92b08f 100644
--- a/cache/cache_test.go
+++ b/cache/cache_test.go
@@ -23,10 +23,10 @@ var (
errTestFailed = errors.New("expected the main handler to be executed %d times instead of %d")
)
-func runTest(e *httpexpect.Expect, counterPtr *uint32, expectedBodyStr string, nocache string) error {
- e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
+func runTest(e *httpexpect.Expect, path string, counterPtr *uint32, expectedBodyStr string, nocache string) error {
+ e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
time.Sleep(cacheDuration / 5) // lets wait for a while, cache should be saved and ready
- e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
+ e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter := atomic.LoadUint32(counterPtr)
if counter > 1 {
// n should be 1 because it doesn't changed after the first call
@@ -35,19 +35,19 @@ func runTest(e *httpexpect.Expect, counterPtr *uint32, expectedBodyStr string, n
time.Sleep(cacheDuration)
// cache should be cleared now
- e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
+ e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
time.Sleep(cacheDuration / 5)
// let's call again , the cache should be saved
- e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
+ e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr)
if counter != 2 {
return errTestFailed.Format(2, counter)
}
- // we have cache response saved for the "/" path, we have some time more here, but here
+ // we have cache response saved for the path, we have some time more here, but here
// we will make the requestS with some of the deniers options
- e.GET("/").WithHeader("max-age", "0").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
- e.GET("/").WithHeader("Authorization", "basic or anything").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
+ e.GET(path).WithHeader("max-age", "0").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
+ e.GET(path).WithHeader("Authorization", "basic or anything").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr)
if counter != 4 {
return errTestFailed.Format(4, counter)
@@ -71,8 +71,8 @@ func runTest(e *httpexpect.Expect, counterPtr *uint32, expectedBodyStr string, n
return errTestFailed.Format(6, counter)
}
- // let's call again the "/", the expiration is not passed so it should be cached
- e.GET("/").Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
+ // let's call again the path the expiration is not passed so it should be cached
+ e.GET(path).Expect().Status(http.StatusOK).Body().Equal(expectedBodyStr)
counter = atomic.LoadUint32(counterPtr)
if counter != 6 {
return errTestFailed.Format(6, counter)
@@ -88,19 +88,19 @@ func TestNoCache(t *testing.T) {
app := iris.New()
var n uint32
- app.Get("/", cache.WrapHandler(func(ctx context.Context) {
+ app.Get("/", cache.Handler(cacheDuration), func(ctx context.Context) {
atomic.AddUint32(&n, 1)
ctx.Write([]byte(expectedBodyStr))
- }, cacheDuration))
+ })
- app.Get("/nocache", cache.WrapHandler(func(ctx context.Context) {
+ app.Get("/nocache", cache.Handler(cacheDuration), func(ctx context.Context) {
cache.NoCache(ctx) // <----
atomic.AddUint32(&n, 1)
ctx.Write([]byte(expectedBodyStr))
- }, cacheDuration))
+ })
e := httptest.New(t, app)
- if err := runTest(e, &n, expectedBodyStr, "/nocache"); err != nil {
+ if err := runTest(e, "/", &n, expectedBodyStr, "/nocache"); err != nil {
t.Fatalf(t.Name()+": %v", err)
}
@@ -117,11 +117,25 @@ func TestCache(t *testing.T) {
ctx.Write([]byte(expectedBodyStr))
})
+ var (
+ n2 uint32
+ expectedBodyStr2 = "This is the other"
+ )
+
+ app.Get("/other", func(ctx context.Context) {
+ atomic.AddUint32(&n2, 1)
+ ctx.Write([]byte(expectedBodyStr2))
+ })
+
e := httptest.New(t, app)
- if err := runTest(e, &n, expectedBodyStr, ""); err != nil {
+ if err := runTest(e, "/", &n, expectedBodyStr, ""); err != nil {
t.Fatalf(t.Name()+": %v", err)
}
+ if err := runTest(e, "/other", &n2, expectedBodyStr2, ""); err != nil {
+ t.Fatalf(t.Name()+" other: %v", err)
+ }
+
}
func TestCacheHandlerParallel(t *testing.T) {
@@ -138,10 +152,10 @@ func TestCacheValidator(t *testing.T) {
ctx.Write([]byte(expectedBodyStr))
}
- validCache := cache.Cache(h, cacheDuration)
- app.Get("/", validCache.ServeHTTP)
+ validCache := cache.Cache(cacheDuration)
+ app.Get("/", validCache.ServeHTTP, h)
- managedCache := cache.Cache(h, cacheDuration)
+ managedCache := cache.Cache(cacheDuration)
managedCache.AddRule(rule.Validator([]rule.PreValidator{
func(ctx context.Context) bool {
if ctx.Request().URL.Path == "/invalid" {
@@ -151,12 +165,7 @@ func TestCacheValidator(t *testing.T) {
},
}, nil))
- managedCache2 := cache.Cache(func(ctx context.Context) {
- atomic.AddUint32(&n, 1)
- ctx.Header("DONT", "DO not cache that response even if it was claimed")
- ctx.Write([]byte(expectedBodyStr))
-
- }, cacheDuration)
+ managedCache2 := cache.Cache(cacheDuration)
managedCache2.AddRule(rule.Validator(nil,
[]rule.PostValidator{
func(ctx context.Context) bool {
@@ -168,10 +177,15 @@ func TestCacheValidator(t *testing.T) {
},
))
- app.Get("/valid", validCache.ServeHTTP)
+ app.Get("/valid", validCache.ServeHTTP, h)
- app.Get("/invalid", managedCache.ServeHTTP)
- app.Get("/invalid2", managedCache2.ServeHTTP)
+ app.Get("/invalid", managedCache.ServeHTTP, h)
+ app.Get("/invalid2", managedCache2.ServeHTTP, func(ctx context.Context) {
+ atomic.AddUint32(&n, 1)
+ ctx.Header("DONT", "DO not cache that response even if it was claimed")
+ ctx.Write([]byte(expectedBodyStr))
+
+ })
e := httptest.New(t, app)
diff --git a/cache/client/handler.go b/cache/client/handler.go
index 6438bbb3..9b41e538 100644
--- a/cache/client/handler.go
+++ b/cache/client/handler.go
@@ -1,6 +1,7 @@
package client
import (
+ "sync"
"time"
"github.com/kataras/iris/cache/cfg"
@@ -10,34 +11,27 @@ import (
)
// Handler the local cache service handler contains
-// the original bodyHandler, the memory cache entry and
+// the original response, the memory cache entry and
// the validator for each of the incoming requests and post responses
type Handler struct {
-
- // bodyHandler the original route's handler.
- // If nil then it tries to take the next handler from the chain.
- bodyHandler context.Handler
-
// Rule optional validators for pre cache and post cache actions
//
// See more at ruleset.go
rule rule.Rule
-
- // entry is the memory cache entry
- entry *entry.Entry
+ // when expires.
+ expiration time.Duration
+ // entries the memory cache stored responses.
+ entries map[string]*entry.Entry
+ mu sync.RWMutex
}
// NewHandler returns a new cached handler for the "bodyHandler"
// which expires every "expiration".
-func NewHandler(bodyHandler context.Handler,
- expiration time.Duration) *Handler {
-
- e := entry.NewEntry(expiration)
-
+func NewHandler(expiration time.Duration) *Handler {
return &Handler{
- bodyHandler: bodyHandler,
- rule: DefaultRuleSet,
- entry: e,
+ rule: DefaultRuleSet,
+ expiration: expiration,
+ entries: make(map[string]*entry.Entry, 0),
}
}
@@ -66,35 +60,53 @@ func (h *Handler) AddRule(r rule.Rule) *Handler {
return h
}
+var emptyHandler = func(ctx context.Context) {
+ ctx.StatusCode(500)
+ ctx.WriteString("cache: empty body handler")
+ ctx.StopExecution()
+}
+
func (h *Handler) ServeHTTP(ctx context.Context) {
// check for pre-cache validators, if at least one of them return false
// for this specific request, then skip the whole cache
- bodyHandler := h.bodyHandler
-
+ bodyHandler := ctx.NextHandler()
if bodyHandler == nil {
- if nextHandler := ctx.NextHandler(); nextHandler != nil {
- // skip prepares the context to move to the next handler if the "nextHandler" has a ctx.Next() inside it,
- // even if it's not executed because it's cached.
- ctx.Skip()
- bodyHandler = nextHandler
- } else {
- ctx.StatusCode(500)
- ctx.WriteString("cache: empty body handler")
- ctx.StopExecution()
- return
- }
+ emptyHandler(ctx)
+ return
}
+ // skip prepares the context to move to the next handler if the "nextHandler" has a ctx.Next() inside it,
+ // even if it's not executed because it's cached.
+ ctx.Skip()
if !h.rule.Claim(ctx) {
bodyHandler(ctx)
return
}
- // check if we have a stored response( it is not expired)
- res, exists := h.entry.Response()
- if !exists {
+ var (
+ response *entry.Response
+ valid = false
+ key = ctx.Path()
+ )
- // if it's not exists, then execute the original handler
+ h.mu.RLock()
+ e, found := h.entries[key]
+ h.mu.RUnlock()
+
+ if found {
+ // the entry is here, .Response will give us
+ // if it's expired or no
+ response, valid = e.Response()
+ } else {
+ // create the entry now.
+ e = entry.NewEntry(h.expiration)
+ h.mu.Lock()
+ h.entries[key] = e
+ h.mu.Unlock()
+ }
+
+ if !valid {
+ // if it's expired, then execute the original handler
// with our custom response recorder response writer
// because the net/http doesn't give us
// a built'n way to get the status code & body
@@ -119,12 +131,12 @@ func (h *Handler) ServeHTTP(ctx context.Context) {
// check for an expiration time if the
// given expiration was not valid then check for GetMaxAge &
// update the response & release the recorder
- h.entry.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request()))
+ e.Reset(recorder.StatusCode(), recorder.Header().Get(cfg.ContentTypeHeader), body, GetMaxAge(ctx.Request()))
return
}
// if it's valid then just write the cached results
- ctx.ContentType(res.ContentType())
- ctx.StatusCode(res.StatusCode())
- ctx.Write(res.Body())
+ ctx.ContentType(response.ContentType())
+ ctx.StatusCode(response.StatusCode())
+ ctx.Write(response.Body())
}