diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..2a586d60038879687dd20f90977af0b719d20703 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Ivanov, Ivan (UG - Computer Science) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..ff05f2fd8bd902ff1bbb64d5c0bdc1468492873f --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +build: + go get +run: + go run . $(file) +test: + go test \ No newline at end of file diff --git a/README.md b/README.md index 391b083923e35535161a0d7536e2ad3745912970..72096c98737a2e374d28cf138eff2f3a5fbf24d6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ -# json-utility +# JSON Utility +## Usage +``` +$ make build +$ make run file=example.json #where example.json is the file we want to process +``` +## Testing +``` +$ make test +``` + +## Dependencies +- You'd need to have GOPATH exported. GOBIN for MACOS for go get command \ No newline at end of file diff --git a/handler.go b/handler.go new file mode 100644 index 0000000000000000000000000000000000000000..b7a9b9bd8d4f7aee4ee94b328b0e937b278eb54d --- /dev/null +++ b/handler.go @@ -0,0 +1,58 @@ +package main + +import ( + "encoding/json" + "log" + "os" + "sync" +) + +//Handles opens the JSON file and calls extractJSON to parse +//the data. Returns the result as a struct. +func Handles(argument string) (urls *URLs, err error) { + jsonFile, err := os.Open(argument) + if err != nil { + log.Println(err) + return nil, err + } + log.Println("Successfully Opened", argument) + //Close when done. + defer jsonFile.Close() + result := ExctractJSON(jsonFile) + return result, err +} + +func PrintResult(result *URLs) { + for k, v := range result.URL { + log.Println("For URL: ", k) + for m, x := range v.Date { + log.Println("↳", m, "there are:", x.Counter, "unique visits.") + } + } +} + +func ExctractJSON(file *os.File) (urls *URLs) { + d := json.NewDecoder(file) + + var loads []Load + visited := URLs{URL: make(map[string]*Dates)} + + err := d.Decode(&loads) + if err != nil { + log.Panic(err) + return + } + wg := new(sync.WaitGroup) + + for _, load := range loads { + if load.TypeRequest != "GET" { + log.Println("Request is invalid: ", load.TypeRequest) + continue + } + wg.Add(1) + go Process(&visited, load, wg) + + } + wg.Wait() + return &visited +} diff --git a/handlertest.go b/handlertest.go new file mode 100644 index 0000000000000000000000000000000000000000..69bd69cd781496426c5af0f3f47c0a044a675934 --- /dev/null +++ b/handlertest.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "log" + "testing" +) + +/* +Standard assert Equals function +*/ +func assertEqual(t *testing.T, a interface{}, b interface{}, message string) { + if a == b { + return + } + if len(message) == 0 { + message = fmt.Sprintf("%v != %v", a, b) + } + t.Fatal(message) +} + +/* +Testing with sample JSON tha websites are visited. +*/ +func TestWebsites(t *testing.T) { + result, err := Handles("test/test-websites.json") + if err != nil { + log.Println(err) + return + } + + assertEqual(t, len(result.URL), 1, "") +} + +/* +Testing if a user visits one URL multiple times but in +different days if it's still going to add it. +*/ +func TestWebsites2(t *testing.T) { + result, err := Handles("test/test-websites2.json") + if err != nil { + log.Println(err) + return + } + assertEqual(t, len(result.URL), 1, "") +} + +/* +Testing if each date gets added. Basic case. 1 user visits 1 page on +2 different dates. 2 Dates should be returned. Also checks if POST +requests get dropped. +*/ +func TestDates(t *testing.T) { + result, err := Handles("test/test-dates.json") + if err != nil { + log.Println(err) + return + } + assertEqual(t, len(result.URL["http://www.someamazingwebsite.com/1"].Date), 2, "") +} + +/* +Testing if each date gets added. Advaced case. 3 user visits 2 page on +3 different dates. 2 Dates should be returned for each website. +*/ +func TestDates2(t *testing.T) { + result, err := Handles("test/test-dates2.json") + if err != nil { + log.Println(err) + return + } + assertEqual(t, len(result.URL["http://www.someamazingwebsite.com/1"].Date), 2, "") + assertEqual(t, len(result.URL["http://www.someamazingwebsite.com/2"].Date), 2, "") +} diff --git a/json-utility b/json-utility new file mode 100755 index 0000000000000000000000000000000000000000..7016e122e48334e254edbffd295222cb94d05229 Binary files /dev/null and b/json-utility differ diff --git a/main.go b/main.go index de079339eb73be6df7f869159b5228254759b755..c336ddadf774bab85e26f61cfed4acf3291105e0 100644 --- a/main.go +++ b/main.go @@ -1,10 +1,5 @@ -/* -Example payload: -userid: A unique ID representing the user -url : The URL the visitor has accessed -type: The HTTP method used to access the URL -timestamp: The timestamp for when the action occurred -*/ +// Package implments JSON parser utility to process a stream of JSON messages +// and calculate the number of unique viewers per day for each URL in the stream. package main import ( @@ -15,23 +10,23 @@ import ( type Load struct { Userid string `json:"userid"` - Url string `json:"url"` + URL string `json:"url"` TypeRequest string `json:"type"` Timestamp int64 `json:"timestamp"` } type URLs struct { - URL map[string]*Dates - mux sync.RWMutex + URL map[string]*Dates //map holding all the Dates a certain URL is visited + mux sync.RWMutex //mutex for race condition } type Dates struct { - Exists map[string]*Visits + Date map[string]*Visits //map holding all visits for certain date } type Visits struct { - mux sync.RWMutex - Counter int - UserIds map[string]bool + mux sync.RWMutex //mutex for race condition + Counter int //Counter for unique visits + UserIds map[string]bool //map holding different user ids } func main() { @@ -40,27 +35,12 @@ func main() { log.Println("USAGE: ./json-utility [path/to/file]") return } - openFile(os.Args[1]) -} - -func openFile(argument string) { - jsonFile, err := os.Open(argument) + //Get the result from a file. + result, err := Handles(os.Args[1]) if err != nil { log.Println(err) return } - log.Println("Successfully Opened", argument) - //Close when done. - defer jsonFile.Close() - result := handler(jsonFile) - - for k, v := range result.URL { - - log.Println("For URL: ", k) - - for m, x := range v.Exists { - log.Println("↳", m, "there are:", x.Counter, "unique visits.") - } - - } + //Print the result + PrintResult(result) } diff --git a/process.go b/process.go new file mode 100644 index 0000000000000000000000000000000000000000..13ab0545c95e994dd47ce61d74589402c6c1ed10 --- /dev/null +++ b/process.go @@ -0,0 +1,73 @@ +package main + +import ( + "log" + "sync" + "time" +) + +//Process handles processing a load given by +func Process(visited *URLs, load Load, wg *sync.WaitGroup) { + //Notify main that this routine is done. + defer wg.Done() + //Time should be accepted both in epoch milliseconds or seconds. + var t string + if time.Now().UnixNano() > load.Timestamp { + t = time.Unix(load.Timestamp/1000, 0).Format("2006-01-02") + } else { + t = time.Unix(load.Timestamp, 0).Format("2006-01-02") + } + + visited.mux.Lock() + + if date, ok := visited.URL[load.URL]; ok { + log.Println("URL exists.") + if visit, ok := date.Date[t]; ok { + log.Println("Date exists.") + if visit.UserIds[load.Userid] { + visited.mux.Unlock() + return //Return as the user has already visited that website for this day once. + } else { + + visit.mux.Lock() + visit.Counter += 1 + visit.UserIds[load.Userid] = true + + visited.mux.Unlock() + visit.mux.Unlock() + return + } + } else { + //Add the Date and a Visit. + entry := Visits{Counter: 0, UserIds: make(map[string]bool)} + entry.mux.Lock() + entry.Counter += 1 + entry.UserIds[load.Userid] = true + entry.mux.Unlock() + + visited.URL[load.URL].Date[t] = &entry + visited.mux.Unlock() + + log.Println("Date and Visit are added.") + return + } + } else { + log.Println("URL doesn't exist.") + //Initialising URL, date and visits for that date. + + entry := Visits{Counter: 0, UserIds: make(map[string]bool)} + entry.mux.Lock() + + dates := Dates{Date: make(map[string]*Visits)} + + entry.Counter += 1 + entry.UserIds[load.Userid] = true + entry.mux.Unlock() + + dates.Date[t] = &entry + + visited.URL[load.URL] = &dates + visited.mux.Unlock() + return + } +} diff --git a/test/test-dates.json b/test/test-dates.json new file mode 100644 index 0000000000000000000000000000000000000000..28c515146fae92b8b942e052705e013b05d8c9bb --- /dev/null +++ b/test/test-dates.json @@ -0,0 +1,14 @@ +[ + { + "userid": "user1", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1360662163000 + }, + { + "userid": "user2", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1160165163000 + } +] \ No newline at end of file diff --git a/test/test-dates2.json b/test/test-dates2.json new file mode 100644 index 0000000000000000000000000000000000000000..5da1d9ae8425699ab0361253bd274b447df6c78f --- /dev/null +++ b/test/test-dates2.json @@ -0,0 +1,38 @@ +[ + { + "userid": "user1", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1360662163000 + }, + { + "userid": "user2", + "url": "http://www.someamazingwebsite.com/3", + "type": "GET", + "timestamp": 1160165163000 + }, + { + "userid": "user3", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1358122163000 + }, + { + "userid": "user1", + "url": "http://www.someamazingwebsite.com/2", + "type": "GET", + "timestamp": 1360662163000 + }, + { + "userid": "user2", + "url": "http://www.someamazingwebsite.com/2", + "type": "GET", + "timestamp": 1160165163000 + }, + { + "userid": "user3", + "url": "http://www.someamazingwebsite.com/2", + "type": "GET", + "timestamp": 1358122163000 + } +] \ No newline at end of file diff --git a/test/test-websites.json b/test/test-websites.json new file mode 100644 index 0000000000000000000000000000000000000000..8a309917ca5683922e50cf7b4e2e2cc1f44678cc --- /dev/null +++ b/test/test-websites.json @@ -0,0 +1,14 @@ +[ + { + "userid": "user1", + "url": "http://www.someamazingwebsite.com/1", + "type": "POST", + "timestamp": 1360662163000 + }, + { + "userid": "user2", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1360662163000 + } +] \ No newline at end of file diff --git a/test/test-websites2.json b/test/test-websites2.json new file mode 100644 index 0000000000000000000000000000000000000000..7b115afa06147f4213136aec7db9ec250e71694e --- /dev/null +++ b/test/test-websites2.json @@ -0,0 +1,21 @@ +[ + { + "userid": "user2", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1360662163000 + }, + { + "userid": "user2", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1360662163000 + }, + { + "userid": "user2", + "url": "http://www.someamazingwebsite.com/1", + "type": "GET", + "timestamp": 1310666163000 + } + +] \ No newline at end of file