<commit_before>/* Copyright 2016-2017 Vector Creations Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package gomatrixserverlib

import (
	"encoding/json"
	"fmt"
	"golang.org/x/crypto/ed25519"
	"time"
)

// An EventReference is a reference to a matrix event.
type EventReference struct {
	// The event ID of the event.
	EventID string
	// The sha256 of the redacted event.
	EventSHA256 Base64String
}

// An EventBuilder is used to build a new event.
type EventBuilder struct {
	// The user ID of the user sending the event.
	Sender string `json:"sender"`
	// The room ID of the room this event is in.
	RoomID string `json:"room_id"`
	// The type of the event.
	Type string `json:"type"`
	// The state_key of the event if the event is a state event or nil if the event is not a state event.
	StateKey *string `json:"state_key,omitempty"`
	// The events that immediately preceeded this event in the room history.
	PrevEvents []EventReference `json:"prev_events"`
	// The events needed to authenticate this event.
	AuthEvents []EventReference `json:"auth_events"`
	// The event ID of the event being redacted if this event is a "m.room.redaction".
	Redacts string `json:"redacts,omitempty"`
	// The depth of the event, This should be one greater than the maximum depth of the previous events.
	// The create event has a depth of 1.
	Depth    int64 `json:"depth"`
	content  []byte
	unsigned []byte
}

// SetContent sets the JSON content key of the event.
func (eb *EventBuilder) SetContent(content interface{}) (err error) {
	eb.content, err = json.Marshal(content)
	return
}

// SetUnsigned sets the JSON unsigned key of the event.
func (eb *EventBuilder) SetUnsigned(unsigned interface{}) (err error) {
	eb.unsigned, err = json.Marshal(unsigned)
	return
}

// An Event is a matrix event.
// The event should always contain valid JSON.
// If the event content hash is invalid then the event is redacted.
// Redacted events contain only the fields covered by the event signature.
type Event struct {
	redacted  bool
	eventJSON []byte
	fields    eventFields
}

type eventFields struct {
	RoomID     string           `json:"room_id"`
	EventID    string           `json:"event_id"`
	Sender     string           `json:"sender"`
	Type       string           `json:"type"`
	StateKey   *string          `json:"state_key"`
	Content    rawJSON          `json:"content"`
	PrevEvents []EventReference `json:"prev_events"`
	AuthEvents []EventReference `json:"auth_events"`
	Redacts    string           `json:"redacts"`
	Depth      int64            `json:"depth"`
}

var emptyEventReferenceList = []EventReference{}

// Build a new Event.
// This is used when a local event is created on this server.
// Call this after filling out the necessary fields.
// This can be called mutliple times on the same builder.
// A different event ID must be supplied each time this is called.
func (eb *EventBuilder) Build(eventID string, now time.Time, origin, keyID string, privateKey ed25519.PrivateKey) (result Event, err error) {
	var event struct {
		EventBuilder
		EventID        string  `json:"event_id"`
		RawContent     rawJSON `json:"content"`
		RawUnsigned    rawJSON `json:"unsigned"`
		OriginServerTS int64   `json:"origin_server_ts"`
		Origin         string  `json:"origin"`
	}
	event.EventBuilder = *eb
	if event.PrevEvents == nil {
		event.PrevEvents = emptyEventReferenceList
	}
	if event.AuthEvents == nil {
		event.AuthEvents = emptyEventReferenceList
	}
	event.RawContent = rawJSON(event.content)
	event.RawUnsigned = rawJSON(event.unsigned)
	event.OriginServerTS = now.UnixNano() / 1000000
	event.Origin = origin
	event.EventID = eventID

	// TODO: Check size limits.

	var eventJSON []byte

	if eventJSON, err = json.Marshal(&event); err != nil {
		return
	}

	if eventJSON, err = addContentHashesToEvent(eventJSON); err != nil {
		return
	}

	if eventJSON, err = signEvent(origin, keyID, privateKey, eventJSON); err != nil {
		return
	}

	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		return
	}

	result.eventJSON = eventJSON
	err = json.Unmarshal(eventJSON, &result.fields)
	return
}

// NewEventFromUntrustedJSON loads a new event from some JSON that may be invalid.
// This checks that the event is valid JSON.
// It also checks the content hashes to ensure the event has not been tampered with.
// This should be used when receiving new events from remote servers.
func NewEventFromUntrustedJSON(eventJSON []byte) (result Event, err error) {
	var event map[string]rawJSON
	if err = json.Unmarshal(eventJSON, &event); err != nil {
		return
	}
	// Synapse removes these keys from events in case a server accidentally added them.
	// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/crypto/event_signing.py#L57-L62
	delete(event, "outlier")
	delete(event, "destinations")
	delete(event, "age_ts")

	// TODO: Check that the event fields are correctly defined.
	// TODO: Check size limits.

	if eventJSON, err = json.Marshal(event); err != nil {
		return
	}

	if err = checkEventContentHash(eventJSON); err != nil {
		result.redacted = true
		// If the content hash doesn't match then we have to discard all non-essential fields
		// because they've been tampered with.
		if eventJSON, err = redactEvent(eventJSON); err != nil {
			return
		}
	}

	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		return
	}

	result.eventJSON = eventJSON
	err = json.Unmarshal(eventJSON, &result.fields)
	return
}

// NewEventFromTrustedJSON loads a new event from some JSON that must be valid.
// This will be more efficient than NewEventFromUntrustedJSON since it can skip cryptographic checks.
// This can be used when loading matrix events from a local database.
func NewEventFromTrustedJSON(eventJSON []byte, redacted bool) (result Event, err error) {
	result.redacted = redacted
	result.eventJSON = eventJSON
	err = json.Unmarshal(eventJSON, &result.fields)
	return
}

// Redacted returns whether the event is redacted.
func (e Event) Redacted() bool { return e.redacted }

// JSON returns the JSON bytes for the event.
func (e Event) JSON() []byte { return e.eventJSON }

// Redact returns a redacted copy of the event.
func (e Event) Redact() Event {
	if e.redacted {
		return e
	}
	eventJSON, err := redactEvent(e.eventJSON)
	if err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err))
	}
	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err))
	}
	return Event{
		redacted:  true,
		eventJSON: eventJSON,
	}
}

// EventReference returns an EventReference for the event.
// The reference can be used to refer to this event from other events.
func (e Event) EventReference() EventReference {
	reference, err := referenceOfEvent(e.eventJSON)
	if err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		// This can be reached if NewEventFromTrustedJSON is given JSON from an untrusted source.
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v (%q)", err, string(e.eventJSON)))
	}
	return reference
}

// Sign returns a copy of the event with an additional signature.
func (e Event) Sign(signingName, keyID string, privateKey ed25519.PrivateKey) Event {
	eventJSON, err := signEvent(signingName, keyID, privateKey, e.eventJSON)
	if err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v (%q)", err, string(e.eventJSON)))
	}
	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v (%q)", err, string(e.eventJSON)))
	}
	return Event{
		redacted:  e.redacted,
		eventJSON: eventJSON,
	}
}

// KeyIDs returns a list of key IDs that the named entity has signed the event with.
func (e Event) KeyIDs(signingName string) []string {
	var event struct {
		Signatures map[string]map[string]rawJSON `json:"signatures"`
	}
	if err := json.Unmarshal(e.eventJSON, &event); err != nil {
		// This should unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err))
	}
	var keyIDs []string
	for keyID := range event.Signatures[signingName] {
		keyIDs = append(keyIDs, keyID)
	}
	return keyIDs
}

// Verify checks a ed25519 signature
func (e Event) Verify(signingName, keyID string, publicKey ed25519.PublicKey) error {
	return verifyEventSignature(signingName, keyID, publicKey, e.eventJSON)
}

// StateKey returns the "state_key" of the event, or the nil if the event is not a state event.
func (e Event) StateKey() *string {
	return e.fields.StateKey
}

// StateKeyEquals returns true if the event is a state event and the "state_key" matches.
func (e Event) StateKeyEquals(stateKey string) bool {
	if e.fields.StateKey == nil {
		return false
	}
	return *e.fields.StateKey == stateKey
}

// EventID returns the event ID of the event.
func (e Event) EventID() string {
	return e.fields.EventID
}

// Sender returns the user ID of the sender of the event.
func (e Event) Sender() string {
	return e.fields.Sender
}

// Type returns the type of the event.
func (e Event) Type() string {
	return e.fields.Type
}

// Content returns the content JSON of the event.
func (e Event) Content() []byte {
	return []byte(e.fields.Content)
}

// PrevEvents returns references to the direct ancestors of the event.
func (e Event) PrevEvents() []EventReference {
	return e.fields.PrevEvents
}

// AuthEvents returns references to the events needed to auth the event.
func (e Event) AuthEvents() []EventReference {
	return e.fields.AuthEvents
}

// Redacts returns the event ID of the event this event redacts.
func (e Event) Redacts() string {
	return e.fields.Redacts
}

// RoomID returns the room ID of the room the event is in.
func (e Event) RoomID() string {
	return e.fields.RoomID
}

// Depth returns the depth of the event.
func (e Event) Depth() int64 {
	return e.fields.Depth
}

// UnmarshalJSON implements json.Unmarshaller assuming the Event is from an untrusted source.
// This will cause more checks than might be necessary but is probably better to be safe than sorry.
func (e *Event) UnmarshalJSON(data []byte) (err error) {
	*e, err = NewEventFromUntrustedJSON(data)
	return
}

// MarshalJSON implements json.Marshaller
func (e Event) MarshalJSON() ([]byte, error) {
	if e.eventJSON == nil {
		return nil, fmt.Errorf("gomatrixserverlib: cannot serialise uninitialised Event")
	}
	return e.eventJSON, nil
}

// UnmarshalJSON implements json.Unmarshaller
func (er *EventReference) UnmarshalJSON(data []byte) error {
	var tuple []rawJSON
	if err := json.Unmarshal(data, &tuple); err != nil {
		return err
	}
	if len(tuple) != 2 {
		return fmt.Errorf("gomatrixserverlib: invalid event reference, invalid length: %d != 2", len(tuple))
	}
	if err := json.Unmarshal(tuple[0], &er.EventID); err != nil {
		return fmt.Errorf("gomatrixserverlib: invalid event reference, first element is invalid: %q %v", string(tuple[0]), err)
	}
	var hashes struct {
		SHA256 Base64String `json:"sha256"`
	}
	if err := json.Unmarshal(tuple[1], &hashes); err != nil {
		return fmt.Errorf("gomatrixserverlib: invalid event reference, second element is invalid: %q %v", string(tuple[1]), err)
	}
	er.EventSHA256 = hashes.SHA256
	return nil
}

// MarshalJSON implements json.Marshaller
func (er EventReference) MarshalJSON() ([]byte, error) {
	hashes := struct {
		SHA256 Base64String `json:"sha256"`
	}{er.EventSHA256}

	tuple := []interface{}{er.EventID, hashes}

	return json.Marshal(&tuple)
}
<commit_msg>Allow callers to not have called Builder.SetUnsigned and still Build() the event<commit_after>/* Copyright 2016-2017 Vector Creations Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package gomatrixserverlib

import (
	"encoding/json"
	"fmt"
	"golang.org/x/crypto/ed25519"
	"time"
)

// An EventReference is a reference to a matrix event.
type EventReference struct {
	// The event ID of the event.
	EventID string
	// The sha256 of the redacted event.
	EventSHA256 Base64String
}

// An EventBuilder is used to build a new event.
type EventBuilder struct {
	// The user ID of the user sending the event.
	Sender string `json:"sender"`
	// The room ID of the room this event is in.
	RoomID string `json:"room_id"`
	// The type of the event.
	Type string `json:"type"`
	// The state_key of the event if the event is a state event or nil if the event is not a state event.
	StateKey *string `json:"state_key,omitempty"`
	// The events that immediately preceeded this event in the room history.
	PrevEvents []EventReference `json:"prev_events"`
	// The events needed to authenticate this event.
	AuthEvents []EventReference `json:"auth_events"`
	// The event ID of the event being redacted if this event is a "m.room.redaction".
	Redacts string `json:"redacts,omitempty"`
	// The depth of the event, This should be one greater than the maximum depth of the previous events.
	// The create event has a depth of 1.
	Depth    int64 `json:"depth"`
	content  []byte
	unsigned []byte
}

// SetContent sets the JSON content key of the event.
func (eb *EventBuilder) SetContent(content interface{}) (err error) {
	eb.content, err = json.Marshal(content)
	return
}

// SetUnsigned sets the JSON unsigned key of the event.
func (eb *EventBuilder) SetUnsigned(unsigned interface{}) (err error) {
	eb.unsigned, err = json.Marshal(unsigned)
	return
}

// An Event is a matrix event.
// The event should always contain valid JSON.
// If the event content hash is invalid then the event is redacted.
// Redacted events contain only the fields covered by the event signature.
type Event struct {
	redacted  bool
	eventJSON []byte
	fields    eventFields
}

type eventFields struct {
	RoomID     string           `json:"room_id"`
	EventID    string           `json:"event_id"`
	Sender     string           `json:"sender"`
	Type       string           `json:"type"`
	StateKey   *string          `json:"state_key"`
	Content    rawJSON          `json:"content"`
	PrevEvents []EventReference `json:"prev_events"`
	AuthEvents []EventReference `json:"auth_events"`
	Redacts    string           `json:"redacts"`
	Depth      int64            `json:"depth"`
}

var emptyEventReferenceList = []EventReference{}

// Build a new Event.
// This is used when a local event is created on this server.
// Call this after filling out the necessary fields.
// This can be called mutliple times on the same builder.
// A different event ID must be supplied each time this is called.
func (eb *EventBuilder) Build(eventID string, now time.Time, origin, keyID string, privateKey ed25519.PrivateKey) (result Event, err error) {
	var event struct {
		EventBuilder
		EventID        string  `json:"event_id"`
		RawContent     rawJSON `json:"content"`
		RawUnsigned    rawJSON `json:"unsigned,omitempty"`
		OriginServerTS int64   `json:"origin_server_ts"`
		Origin         string  `json:"origin"`
	}
	event.EventBuilder = *eb
	if event.PrevEvents == nil {
		event.PrevEvents = emptyEventReferenceList
	}
	if event.AuthEvents == nil {
		event.AuthEvents = emptyEventReferenceList
	}
	event.RawContent = rawJSON(event.content)
	event.RawUnsigned = rawJSON(event.unsigned)
	event.OriginServerTS = now.UnixNano() / 1000000
	event.Origin = origin
	event.EventID = eventID

	// TODO: Check size limits.

	var eventJSON []byte

	if eventJSON, err = json.Marshal(&event); err != nil {
		return
	}

	if eventJSON, err = addContentHashesToEvent(eventJSON); err != nil {
		return
	}

	if eventJSON, err = signEvent(origin, keyID, privateKey, eventJSON); err != nil {
		return
	}

	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		return
	}

	result.eventJSON = eventJSON
	err = json.Unmarshal(eventJSON, &result.fields)
	return
}

// NewEventFromUntrustedJSON loads a new event from some JSON that may be invalid.
// This checks that the event is valid JSON.
// It also checks the content hashes to ensure the event has not been tampered with.
// This should be used when receiving new events from remote servers.
func NewEventFromUntrustedJSON(eventJSON []byte) (result Event, err error) {
	var event map[string]rawJSON
	if err = json.Unmarshal(eventJSON, &event); err != nil {
		return
	}
	// Synapse removes these keys from events in case a server accidentally added them.
	// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/crypto/event_signing.py#L57-L62
	delete(event, "outlier")
	delete(event, "destinations")
	delete(event, "age_ts")

	// TODO: Check that the event fields are correctly defined.
	// TODO: Check size limits.

	if eventJSON, err = json.Marshal(event); err != nil {
		return
	}

	if err = checkEventContentHash(eventJSON); err != nil {
		result.redacted = true
		// If the content hash doesn't match then we have to discard all non-essential fields
		// because they've been tampered with.
		if eventJSON, err = redactEvent(eventJSON); err != nil {
			return
		}
	}

	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		return
	}

	result.eventJSON = eventJSON
	err = json.Unmarshal(eventJSON, &result.fields)
	return
}

// NewEventFromTrustedJSON loads a new event from some JSON that must be valid.
// This will be more efficient than NewEventFromUntrustedJSON since it can skip cryptographic checks.
// This can be used when loading matrix events from a local database.
func NewEventFromTrustedJSON(eventJSON []byte, redacted bool) (result Event, err error) {
	result.redacted = redacted
	result.eventJSON = eventJSON
	err = json.Unmarshal(eventJSON, &result.fields)
	return
}

// Redacted returns whether the event is redacted.
func (e Event) Redacted() bool { return e.redacted }

// JSON returns the JSON bytes for the event.
func (e Event) JSON() []byte { return e.eventJSON }

// Redact returns a redacted copy of the event.
func (e Event) Redact() Event {
	if e.redacted {
		return e
	}
	eventJSON, err := redactEvent(e.eventJSON)
	if err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err))
	}
	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err))
	}
	return Event{
		redacted:  true,
		eventJSON: eventJSON,
	}
}

// EventReference returns an EventReference for the event.
// The reference can be used to refer to this event from other events.
func (e Event) EventReference() EventReference {
	reference, err := referenceOfEvent(e.eventJSON)
	if err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		// This can be reached if NewEventFromTrustedJSON is given JSON from an untrusted source.
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v (%q)", err, string(e.eventJSON)))
	}
	return reference
}

// Sign returns a copy of the event with an additional signature.
func (e Event) Sign(signingName, keyID string, privateKey ed25519.PrivateKey) Event {
	eventJSON, err := signEvent(signingName, keyID, privateKey, e.eventJSON)
	if err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v (%q)", err, string(e.eventJSON)))
	}
	if eventJSON, err = CanonicalJSON(eventJSON); err != nil {
		// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v (%q)", err, string(e.eventJSON)))
	}
	return Event{
		redacted:  e.redacted,
		eventJSON: eventJSON,
	}
}

// KeyIDs returns a list of key IDs that the named entity has signed the event with.
func (e Event) KeyIDs(signingName string) []string {
	var event struct {
		Signatures map[string]map[string]rawJSON `json:"signatures"`
	}
	if err := json.Unmarshal(e.eventJSON, &event); err != nil {
		// This should unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
		panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err))
	}
	var keyIDs []string
	for keyID := range event.Signatures[signingName] {
		keyIDs = append(keyIDs, keyID)
	}
	return keyIDs
}

// Verify checks a ed25519 signature
func (e Event) Verify(signingName, keyID string, publicKey ed25519.PublicKey) error {
	return verifyEventSignature(signingName, keyID, publicKey, e.eventJSON)
}

// StateKey returns the "state_key" of the event, or the nil if the event is not a state event.
func (e Event) StateKey() *string {
	return e.fields.StateKey
}

// StateKeyEquals returns true if the event is a state event and the "state_key" matches.
func (e Event) StateKeyEquals(stateKey string) bool {
	if e.fields.StateKey == nil {
		return false
	}
	return *e.fields.StateKey == stateKey
}

// EventID returns the event ID of the event.
func (e Event) EventID() string {
	return e.fields.EventID
}

// Sender returns the user ID of the sender of the event.
func (e Event) Sender() string {
	return e.fields.Sender
}

// Type returns the type of the event.
func (e Event) Type() string {
	return e.fields.Type
}

// Content returns the content JSON of the event.
func (e Event) Content() []byte {
	return []byte(e.fields.Content)
}

// PrevEvents returns references to the direct ancestors of the event.
func (e Event) PrevEvents() []EventReference {
	return e.fields.PrevEvents
}

// AuthEvents returns references to the events needed to auth the event.
func (e Event) AuthEvents() []EventReference {
	return e.fields.AuthEvents
}

// Redacts returns the event ID of the event this event redacts.
func (e Event) Redacts() string {
	return e.fields.Redacts
}

// RoomID returns the room ID of the room the event is in.
func (e Event) RoomID() string {
	return e.fields.RoomID
}

// Depth returns the depth of the event.
func (e Event) Depth() int64 {
	return e.fields.Depth
}

// UnmarshalJSON implements json.Unmarshaller assuming the Event is from an untrusted source.
// This will cause more checks than might be necessary but is probably better to be safe than sorry.
func (e *Event) UnmarshalJSON(data []byte) (err error) {
	*e, err = NewEventFromUntrustedJSON(data)
	return
}

// MarshalJSON implements json.Marshaller
func (e Event) MarshalJSON() ([]byte, error) {
	if e.eventJSON == nil {
		return nil, fmt.Errorf("gomatrixserverlib: cannot serialise uninitialised Event")
	}
	return e.eventJSON, nil
}

// UnmarshalJSON implements json.Unmarshaller
func (er *EventReference) UnmarshalJSON(data []byte) error {
	var tuple []rawJSON
	if err := json.Unmarshal(data, &tuple); err != nil {
		return err
	}
	if len(tuple) != 2 {
		return fmt.Errorf("gomatrixserverlib: invalid event reference, invalid length: %d != 2", len(tuple))
	}
	if err := json.Unmarshal(tuple[0], &er.EventID); err != nil {
		return fmt.Errorf("gomatrixserverlib: invalid event reference, first element is invalid: %q %v", string(tuple[0]), err)
	}
	var hashes struct {
		SHA256 Base64String `json:"sha256"`
	}
	if err := json.Unmarshal(tuple[1], &hashes); err != nil {
		return fmt.Errorf("gomatrixserverlib: invalid event reference, second element is invalid: %q %v", string(tuple[1]), err)
	}
	er.EventSHA256 = hashes.SHA256
	return nil
}

// MarshalJSON implements json.Marshaller
func (er EventReference) MarshalJSON() ([]byte, error) {
	hashes := struct {
		SHA256 Base64String `json:"sha256"`
	}{er.EventSHA256}

	tuple := []interface{}{er.EventID, hashes}

	return json.Marshal(&tuple)
}
<|endoftext|>


<commit_before>// Copyright 2016 The nvim-go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package nvimutil

import (
	"fmt"
	"log"
	"strings"

	"nvim-go/config"

	"github.com/neovim/go-client/nvim"
	"github.com/pkg/errors"
)

var (
	// ErrorColor highlight error message use Identifier syntax color.
	ErrorColor = "Identifier"
	// ProgressColor highlight progress message use Identifier syntax color.
	ProgressColor = "Identifier"
	// SuccessColor highlight success message use Identifier syntax color.
	SuccessColor = "Function"
)

// Echo provide the vim 'echo' command.
func Echo(v *nvim.Nvim, format string, a ...interface{}) error {
	v.Command("redraw")
	return v.Command("echo '" + fmt.Sprintf(format, a...) + "'")
}

// EchoRaw provide the raw output vim 'echo' command.
func EchoRaw(v *nvim.Nvim, a string) error {
	v.Command("redraw")
	return v.Command("echo \"" + a + "\"")
}

// Echomsg provide the vim 'echomsg' command.
func Echomsg(v *nvim.Nvim, a ...interface{}) error {
	v.Command("redraw")
	return v.Command("echomsg '" + strings.TrimSpace(fmt.Sprintln(a...)) + "'")
}

// Echoerr provide the vim 'echoerr' command.
func Echoerr(v *nvim.Nvim, format string, a ...interface{}) error {
	v.Command("redraw")
	return v.Command("echoerr '" + fmt.Sprintf(format, a...) + "'")
}

type stackTracer interface {
	StackTrace() errors.StackTrace
}

// ErrorWrap splits the errors.Wrap's cause and error messages,
// and provide the vim 'echo' message with 'echohl' highlighting to cause text.
func ErrorWrap(v *nvim.Nvim, err error) error {
	if err == nil {
		return nil
	}

	var funcName string
	if err, ok := err.(stackTracer); ok {
		st := err.StackTrace()
		// "%n" verb is function name
		funcName = fmt.Sprintf("%n", st[0])
		if config.DebugEnable {
			log.Printf("Error stack%+v", st[:])
		}
	}
	// fallback use plugin name
	if funcName == "" {
		funcName = "nvim-go"
	}

	return v.WritelnErr(fmt.Sprintf("%s: %s", funcName, err))
}

// EchohlBefore provide the vim 'echo' command with the 'echohl' highlighting prefix text.
func EchohlBefore(v *nvim.Nvim, prefix string, highlight string, format string, a ...interface{}) error {
	v.Command("redraw")
	suffix := "\" | echohl None | echon \""
	if prefix != "" {
		suffix += ": "
	}
	return v.Command("echohl " + highlight + " | echo \"" + prefix + suffix + fmt.Sprintf(format, a...) + "\" | echohl None")
}

// EchohlAfter provide the vim 'echo' command with the 'echohl' highlighting message text.
func EchohlAfter(v *nvim.Nvim, prefix string, highlight string, format string, a ...interface{}) error {
	v.Command("redraw")
	if prefix != "" {
		prefix += ": "
	}
	return v.Command("echo \"" + prefix + "\" | echohl " + highlight + " | echon \"" + fmt.Sprintf(format, a...) + "\" | echohl None")
}

// EchoProgress displays a command progress message to echo area.
func EchoProgress(v *nvim.Nvim, prefix, format string, a ...interface{}) error {
	v.Command("redraw")
	msg := fmt.Sprintf(format, a...)
	return v.Command(fmt.Sprintf("echo \"%s: \" | echohl %s | echon \"%s ...\" | echohl None", prefix, ProgressColor, msg))
}

// EchoSuccess displays the success of the command to echo area.
func EchoSuccess(v *nvim.Nvim, prefix string, msg string) error {
	v.Command("redraw")
	if msg != "" {
		msg = " | " + msg
	}
	return v.Command(fmt.Sprintf("echo \"%s: \" | echohl %s | echon 'SUCCESS' | echohl None | echon '%s' | echohl None", prefix, SuccessColor, msg))
}

// ReportError output of the accumulated errors report.
// TODO(zchee): research vim.ReportError behavior
// Why it does not immediately display error?
// func ReportError(v *vim.Nvim, format string, a ...interface{}) error {
// 	return v.ReportError(fmt.Sprintf(format, a...))
// }

// ClearMsg cleanups the echo area.
func ClearMsg(v *nvim.Nvim) error {
	return v.Command("echon")
}
<commit_msg>nvimutil/echo: Use nvim.Write{Out,lnErr} instead of<commit_after>// Copyright 2016 The nvim-go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package nvimutil

import (
	"fmt"
	"log"

	"nvim-go/config"

	"github.com/neovim/go-client/nvim"
	"github.com/pkg/errors"
)

var (
	// ErrorColor highlight error message use Identifier syntax color.
	ErrorColor = "Identifier"
	// ProgressColor highlight progress message use Identifier syntax color.
	ProgressColor = "Identifier"
	// SuccessColor highlight success message use Identifier syntax color.
	SuccessColor = "Function"
)

// Echo provide the vim 'echo' command.
func Echo(v *nvim.Nvim, format string, a ...interface{}) error {
	v.Command("redraw")
	return v.Command("echo '" + fmt.Sprintf(format, a...) + "'")
}

// EchoRaw provide the raw output vim 'echo' command.
func EchoRaw(v *nvim.Nvim, a string) error {
	v.Command("redraw")
	return v.Command("echo \"" + a + "\"")
}

// Echomsg provide the vim 'echomsg' command.
func Echomsg(v *nvim.Nvim, a ...interface{}) error {
	return v.WriteOut(fmt.Sprintln(a...))
}

// Echoerr provide the vim 'echoerr' command.
func Echoerr(v *nvim.Nvim, format string, a ...interface{}) error {
	return v.WritelnErr(fmt.Sprintf(format, a...))
}

type stackTracer interface {
	StackTrace() errors.StackTrace
}

// ErrorWrap splits the errors.Wrap's cause and error messages,
// and provide the vim 'echo' message with 'echohl' highlighting to cause text.
func ErrorWrap(v *nvim.Nvim, err error) error {
	if err == nil {
		return nil
	}

	var funcName string
	if err, ok := err.(stackTracer); ok {
		st := err.StackTrace()
		// "%n" verb is function name
		funcName = fmt.Sprintf("%n", st[0])
		if config.DebugEnable {
			log.Printf("Error stack%+v", st[:])
		}
	}
	// fallback use plugin name
	if funcName == "" {
		funcName = "nvim-go"
	}

	return v.WritelnErr(fmt.Sprintf("%s: %s", funcName, err))
}

// EchohlBefore provide the vim 'echo' command with the 'echohl' highlighting prefix text.
func EchohlBefore(v *nvim.Nvim, prefix string, highlight string, format string, a ...interface{}) error {
	v.Command("redraw")
	suffix := "\" | echohl None | echon \""
	if prefix != "" {
		suffix += ": "
	}
	return v.Command("echohl " + highlight + " | echo \"" + prefix + suffix + fmt.Sprintf(format, a...) + "\" | echohl None")
}

// EchohlAfter provide the vim 'echo' command with the 'echohl' highlighting message text.
func EchohlAfter(v *nvim.Nvim, prefix string, highlight string, format string, a ...interface{}) error {
	v.Command("redraw")
	if prefix != "" {
		prefix += ": "
	}
	return v.Command("echo \"" + prefix + "\" | echohl " + highlight + " | echon \"" + fmt.Sprintf(format, a...) + "\" | echohl None")
}

// EchoProgress displays a command progress message to echo area.
func EchoProgress(v *nvim.Nvim, prefix, format string, a ...interface{}) error {
	v.Command("redraw")
	msg := fmt.Sprintf(format, a...)
	return v.Command(fmt.Sprintf("echo \"%s: \" | echohl %s | echon \"%s ...\" | echohl None", prefix, ProgressColor, msg))
}

// EchoSuccess displays the success of the command to echo area.
func EchoSuccess(v *nvim.Nvim, prefix string, msg string) error {
	v.Command("redraw")
	if msg != "" {
		msg = " | " + msg
	}
	return v.Command(fmt.Sprintf("echo \"%s: \" | echohl %s | echon 'SUCCESS' | echohl None | echon '%s' | echohl None", prefix, SuccessColor, msg))
}

// ReportError output of the accumulated errors report.
// TODO(zchee): research vim.ReportError behavior
// Why it does not immediately display error?
// func ReportError(v *vim.Nvim, format string, a ...interface{}) error {
// 	return v.ReportError(fmt.Sprintf(format, a...))
// }

// ClearMsg cleanups the echo area.
func ClearMsg(v *nvim.Nvim) error {
	return v.Command("echon")
}
<|endoftext|>


<commit_before>package main

import (
	"log"
	"net/http"

	"github.com/emicklei/go-restful"
	restfulspec "github.com/emicklei/go-restful-openapi"
	"github.com/go-openapi/spec"
)

type UserResource struct {
	// normally one would use DAO (data access object)
	users map[string]User
}

func (u UserResource) WebService() *restful.WebService {
	ws := new(restful.WebService)
	ws.
		Path("/users").
		Consumes(restful.MIME_XML, restful.MIME_JSON).
		Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well

	tags := []string{"users"}

	ws.Route(ws.GET("/").To(u.findAllUsers).
		// docs
		Doc("get all users").
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Writes([]User{}).
		Returns(200, "OK", []User{}))

	ws.Route(ws.GET("/{user-id}").To(u.findUser).
		// docs
		Doc("get a user").
		Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Writes(User{}). // on the response
		Returns(200, "OK", User{}).
		Returns(404, "Not Found", nil))

	ws.Route(ws.PUT("/{user-id}").To(u.updateUser).
		// docs
		Doc("update a user").
		Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Reads(User{})) // from the request

	ws.Route(ws.PUT("").To(u.createUser).
		// docs
		Doc("create a user").
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Reads(User{})) // from the request

	ws.Route(ws.DELETE("/{user-id}").To(u.removeUser).
		// docs
		Doc("delete a user").
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")))

	return ws
}

// GET http://localhost:8080/users
//
func (u UserResource) findAllUsers(request *restful.Request, response *restful.Response) {
	list := []User{}
	for _, each := range u.users {
		list = append(list, each)
	}
	response.WriteEntity(list)
}

// GET http://localhost:8080/users/1
//
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
	id := request.PathParameter("user-id")
	usr := u.users[id]
	if len(usr.ID) == 0 {
		response.WriteErrorString(http.StatusNotFound, "User could not be found.")
	} else {
		response.WriteEntity(usr)
	}
}

// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) {
	usr := new(User)
	err := request.ReadEntity(&usr)
	if err == nil {
		u.users[usr.ID] = *usr
		response.WriteEntity(usr)
	} else {
		response.WriteError(http.StatusInternalServerError, err)
	}
}

// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
	usr := User{ID: request.PathParameter("user-id")}
	err := request.ReadEntity(&usr)
	if err == nil {
		u.users[usr.ID] = usr
		response.WriteHeaderAndEntity(http.StatusCreated, usr)
	} else {
		response.WriteError(http.StatusInternalServerError, err)
	}
}

// DELETE http://localhost:8080/users/1
//
func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) {
	id := request.PathParameter("user-id")
	delete(u.users, id)
}

func main() {
	u := UserResource{map[string]User{}}
	restful.DefaultContainer.Add(u.WebService())

	config := restfulspec.Config{
		WebServices:    restful.RegisteredWebServices(), // you control what services are visible
		WebServicesURL: "http://localhost:8080",
		APIPath:        "/apidocs.json",
		PostBuildSwaggerObjectHandler: enrichSwaggerObject}
	restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config))

	// Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
	// You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
	// Open http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json
	http.Handle("/apidocs/", http.StripPrefix("/apidocs/", http.FileServer(http.Dir("/Users/emicklei/xProjects/swagger-ui/dist"))))

	log.Printf("start listening on localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func enrichSwaggerObject(swo *spec.Swagger) {
	swo.Info = &spec.Info{
		InfoProps: spec.InfoProps{
			Title:       "UserService",
			Description: "Resource for managing Users",
			Contact: &spec.ContactInfo{
				Name:  "john",
				Email: "john@doe.rp",
				URL:   "http://johndoe.org",
			},
			License: &spec.License{
				Name: "MIT",
				URL:  "http://mit.org",
			},
			Version: "1.0.0",
		},
	}
	swo.Tags = []spec.Tag{spec.Tag{TagProps: spec.TagProps{
		Name:        "users",
		Description: "Managing users"}}}
}

// User is just a sample type
type User struct {
	ID   string `json:"id" description:"identifier of the user"`
	Name string `json:"name" description:"name of the user"`
}
<commit_msg>provide more complex example<commit_after>package main

import (
	"log"
	"net/http"

	"github.com/emicklei/go-restful"
	restfulspec "github.com/emicklei/go-restful-openapi"
	"github.com/go-openapi/spec"
)

type UserResource struct {
	// normally one would use DAO (data access object)
	users map[string]User
}

func (u UserResource) WebService() *restful.WebService {
	ws := new(restful.WebService)
	ws.
		Path("/users").
		Consumes(restful.MIME_XML, restful.MIME_JSON).
		Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well

	tags := []string{"users"}

	ws.Route(ws.GET("/").To(u.findAllUsers).
		// docs
		Doc("get all users").
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Writes([]User{}).
		Returns(200, "OK", []User{}))

	ws.Route(ws.GET("/{user-id}").To(u.findUser).
		// docs
		Doc("get a user").
		Param(ws.PathParameter("user-id", "identifier of the user").DataType("integer").DefaultValue("1")).
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Writes(User{}). // on the response
		Returns(200, "OK", User{}).
		Returns(404, "Not Found", nil))

	ws.Route(ws.PUT("/{user-id}").To(u.updateUser).
		// docs
		Doc("update a user").
		Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")).
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Reads(User{})) // from the request

	ws.Route(ws.PUT("").To(u.createUser).
		// docs
		Doc("create a user").
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Reads(User{})) // from the request

	ws.Route(ws.DELETE("/{user-id}").To(u.removeUser).
		// docs
		Doc("delete a user").
		Metadata(restfulspec.KeyOpenAPITags, tags).
		Param(ws.PathParameter("user-id", "identifier of the user").DataType("string")))

	return ws
}

// GET http://localhost:8080/users
//
func (u UserResource) findAllUsers(request *restful.Request, response *restful.Response) {
	list := []User{}
	for _, each := range u.users {
		list = append(list, each)
	}
	response.WriteEntity(list)
}

// GET http://localhost:8080/users/1
//
func (u UserResource) findUser(request *restful.Request, response *restful.Response) {
	id := request.PathParameter("user-id")
	usr := u.users[id]
	if len(usr.ID) == 0 {
		response.WriteErrorString(http.StatusNotFound, "User could not be found.")
	} else {
		response.WriteEntity(usr)
	}
}

// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa Raspberry</Name></User>
//
func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) {
	usr := new(User)
	err := request.ReadEntity(&usr)
	if err == nil {
		u.users[usr.ID] = *usr
		response.WriteEntity(usr)
	} else {
		response.WriteError(http.StatusInternalServerError, err)
	}
}

// PUT http://localhost:8080/users/1
// <User><Id>1</Id><Name>Melissa</Name></User>
//
func (u *UserResource) createUser(request *restful.Request, response *restful.Response) {
	usr := User{ID: request.PathParameter("user-id")}
	err := request.ReadEntity(&usr)
	if err == nil {
		u.users[usr.ID] = usr
		response.WriteHeaderAndEntity(http.StatusCreated, usr)
	} else {
		response.WriteError(http.StatusInternalServerError, err)
	}
}

// DELETE http://localhost:8080/users/1
//
func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) {
	id := request.PathParameter("user-id")
	delete(u.users, id)
}

func main() {
	u := UserResource{map[string]User{}}
	restful.DefaultContainer.Add(u.WebService())

	config := restfulspec.Config{
		WebServices:    restful.RegisteredWebServices(), // you control what services are visible
		WebServicesURL: "http://localhost:8080",
		APIPath:        "/apidocs.json",
		PostBuildSwaggerObjectHandler: enrichSwaggerObject}
	restful.DefaultContainer.Add(restfulspec.NewOpenAPIService(config))

	// Optionally, you can install the Swagger Service which provides a nice Web UI on your REST API
	// You need to download the Swagger HTML5 assets and change the FilePath location in the config below.
	// Open http://localhost:8080/apidocs/?url=http://localhost:8080/apidocs.json
	http.Handle("/apidocs/", http.StripPrefix("/apidocs/", http.FileServer(http.Dir("/home/v/Downloads/swagger-ui"))))

	log.Printf("start listening on localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func enrichSwaggerObject(swo *spec.Swagger) {
	swo.Info = &spec.Info{
		InfoProps: spec.InfoProps{
			Title:       "UserService",
			Description: "Resource for managing Users",
			Contact: &spec.ContactInfo{
				Name:  "john",
				Email: "john@doe.rp",
				URL:   "http://johndoe.org",
			},
			License: &spec.License{
				Name: "MIT",
				URL:  "http://mit.org",
			},
			Version: "1.0.0",
		},
	}
	swo.Tags = []spec.Tag{spec.Tag{TagProps: spec.TagProps{
		Name:        "users",
		Description: "Managing users"}}}
}

// User is just a sample type
type User struct {
	ID   string `json:"id" description:"identifier of the user"`
	Name string `json:"name" description:"name of the user" default:"john"`
	Age  int    `json:"age" description:"age of the user" default:"21"`
}
<|endoftext|>


<commit_before>package numbers

import (
	"bytes"
	"encoding/csv"
	"fmt"
	"github.com/coreos/go-etcd/etcd"
	log "github.com/gonet2/nsq-logger"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	DEFAULT_NUMBERS_PATH = "/numbers"
	DEFAULT_ETCD         = "http://127.0.0.1:2379"
	RETRY_DELAY          = 10 * time.Second
)

var (
	_default_numbers numbers
)

func init() {
	_default_numbers.init()
	_default_numbers.load(DEFAULT_NUMBERS_PATH)
	go _default_numbers.watcher()
}

// a record contains fields
type record struct {
	fields map[string]string
}

// a table contains records
type table struct {
	records map[string]*record // all records
	keys    []string           // all record keys
}

// numbers contains all tables
type numbers struct {
	tables      map[string]*table
	client_pool sync.Pool // etcd client pool
	sync.RWMutex
}

func (ns *numbers) init() {
	machines := []string{DEFAULT_ETCD}
	if env := os.Getenv("ETCD_HOST"); env != "" {
		machines = strings.Split(env, ";")
	}

	ns.client_pool.New = func() interface{} {
		return etcd.NewClient(machines)
	}
}

// load all csv(s) from a given directory in etcd
func (ns *numbers) load(directory string) {
	client := ns.client_pool.Get().(*etcd.Client)
	defer func() {
		ns.client_pool.Put(client)
	}()

	// get the keys under directory
	log.Info("loading numbers from:", directory)
	resp, err := client.Get(directory, true, false)
	if err != nil {
		log.Error(err)
		return
	}

	// validation check
	if !resp.Node.Dir {
		log.Error("not a directory")
		return
	}

	// read all csv(s) from this dirctory
	ns.tables = make(map[string]*table)
	count := 0
	for _, node := range resp.Node.Nodes {
		if !node.Dir {
			log.Trace("loading:", node.Key)
			ns.parse(node.Key, node.Value)
			count++
		} else {
			log.Warning("not a file:", node.Key)
		}
	}

	log.Finef("%v csv(s) loaded", count)
}

// watcher for data change in etcd directory
func (ns *numbers) watcher() {
	client := ns.client_pool.Get().(*etcd.Client)
	defer func() {
		ns.client_pool.Put(client)
	}()

	for {
		ch := make(chan *etcd.Response, 10)
		go func() {
			for {
				if resp, ok := <-ch; ok {
					if !resp.Node.Dir {
						ns.parse(resp.Node.Key, resp.Node.Value)
						log.Trace("csv change:", resp.Node.Key)
					}
				} else {
					return
				}
			}
		}()

		_, err := client.Watch(DEFAULT_NUMBERS_PATH, 0, true, ch, nil)
		if err != nil {
			log.Critical(err)
		}
		<-time.After(RETRY_DELAY)
	}
}

// parse & load a csv
func (ns *numbers) parse(key, value string) {
	ns.Lock()
	defer ns.Unlock()
	src := bytes.NewBufferString(value)
	csv_reader := csv.NewReader(src)
	records, err := csv_reader.ReadAll()
	if err != nil {
		log.Errorf("%v %v", err, key)
		return
	}

	if len(records) == 0 {
		log.Warningf("empty document: %v", key)
		return
	}

	tblname := filepath.Base(key)
	// 记录数据, 第一行为表头，因此从第二行开始
	for line := 1; line < len(records); line++ {
		for field := 1; field < len(records[line]); field++ { // 每条记录的第一个字段作为行索引
			ns.set(ns.tables, tblname, records[line][0], records[0][field], records[line][field])
		}
	}

	// 记录KEYS
	ns.dump_keys(ns.tables, tblname)
}

// set field value
func (ns *numbers) set(tables map[string]*table, tblname string, rowname string, fieldname string, value string) {
	tbl, ok := tables[tblname]
	if !ok {
		tbl = &table{}
		tbl.records = make(map[string]*record)
		tables[tblname] = tbl
	}

	rec, ok := tbl.records[rowname]
	if !ok {
		rec = &record{}
		rec.fields = make(map[string]string)
		tbl.records[rowname] = rec
	}

	rec.fields[fieldname] = value
}

// dump keys
func (ns *numbers) dump_keys(tables map[string]*table, tblname string) {
	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	for k := range tbl.records {
		tbl.keys = append(tbl.keys, k)
	}
}

// get field value
func (ns *numbers) get(tblname string, rowname string, fieldname string) string {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	rec, ok := tbl.records[rowname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " row ", rowname, " not exists!"))
	}

	value, ok := rec.fields[fieldname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " field ", fieldname, " not exists!"))
	}
	return value
}

// get field value as integer
func (ns *numbers) GetInt(tblname string, rowname interface{}, fieldname string) int32 {
	val := ns.get(tblname, fmt.Sprint(rowname), fieldname)
	if val == "" {
		return 0
	}

	v, err := strconv.ParseFloat(val, 64)
	if err != nil {
		panic(fmt.Sprintf("cannot parse integer from gamedata %v %v %v %v\n", tblname, rowname, fieldname, err))
	}

	// round to the integer
	// 1.00000001 -> 1
	// 0.99999999 -> 1
	// -0.9999990 -> -1
	// -1.0000001 -> -1
	return int32(v)
}

// get field value as float
func (ns *numbers) GetFloat(tblname string, rowname interface{}, fieldname string) float64 {
	val := ns.get(tblname, fmt.Sprint(rowname), fieldname)
	if val == "" {
		return 0.0
	}

	f, err := strconv.ParseFloat(val, 64)
	if err != nil {
		panic(fmt.Sprintf("cannot parse float from gamedata %v %v %v %v\n", tblname, rowname, fieldname, err))
	}

	return f
}

// get field value as string
func (ns *numbers) GetString(tblname string, rowname interface{}, fieldname string) string {
	return ns.get(tblname, fmt.Sprint(rowname), fieldname)
}

// get all keys
func (ns *numbers) GetKeys(tblname string) []string {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	return tbl.keys
}

// get row count
func (ns *numbers) Count(tblname string) int32 {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	return int32(len(tbl.records))
}

// test record exists
func (ns *numbers) IsRecordExists(tblname string, rowname interface{}) bool {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		return false
	}

	_, ok = tbl.records[fmt.Sprint(rowname)]
	if !ok {
		return false
	}

	return true
}

// test field exists
func (ns *numbers) IsFieldExists(tblname string, fieldname string) bool {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	// check table existence
	tbl, ok := tables[tblname]
	if !ok {
		return false
	}

	// get one record key
	key := ""
	for k, _ := range tbl.records {
		key = k
		break
	}

	rec, ok := tbl.records[key]
	if !ok {
		return false
	}

	_, ok = rec.fields[fieldname]
	if !ok {
		return false
	}

	return true
}

func GetInt(tblname string, rowname interface{}, fieldname string) int32 {
	return _default_numbers.GetInt(tblname, rowname, fieldname)
}

func GetFloat(tblname string, rowname interface{}, fieldname string) float64 {
	return _default_numbers.GetFloat(tblname, rowname, fieldname)
}

func GetString(tblname string, rowname interface{}, fieldname string) string {
	return _default_numbers.GetString(tblname, rowname, fieldname)
}

func GetKeys(tblname string) []string {
	return _default_numbers.GetKeys(tblname)
}

func Count(tblname string) int32 {
	return _default_numbers.Count(tblname)
}

func IsFieldExists(tblname string, fieldname string) bool {
	return _default_numbers.IsFieldExists(tblname, fieldname)
}

func IsRecordExists(tblname string, rowname interface{}) bool {
	return _default_numbers.IsRecordExists(tblname, rowname)
}
<commit_msg>update default value<commit_after>package numbers

import (
	"bytes"
	"encoding/csv"
	"fmt"
	"github.com/coreos/go-etcd/etcd"
	log "github.com/gonet2/nsq-logger"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"time"
)

const (
	DEFAULT_NUMBERS_PATH = "/numbers"
	DEFAULT_ETCD         = "http://172.17.42.1:2379"
	RETRY_DELAY          = 10 * time.Second
)

var (
	_default_numbers numbers
)

func init() {
	_default_numbers.init()
	_default_numbers.load(DEFAULT_NUMBERS_PATH)
	go _default_numbers.watcher()
}

// a record contains fields
type record struct {
	fields map[string]string
}

// a table contains records
type table struct {
	records map[string]*record // all records
	keys    []string           // all record keys
}

// numbers contains all tables
type numbers struct {
	tables      map[string]*table
	client_pool sync.Pool // etcd client pool
	sync.RWMutex
}

func (ns *numbers) init() {
	machines := []string{DEFAULT_ETCD}
	if env := os.Getenv("ETCD_HOST"); env != "" {
		machines = strings.Split(env, ";")
	}

	ns.client_pool.New = func() interface{} {
		return etcd.NewClient(machines)
	}
}

// load all csv(s) from a given directory in etcd
func (ns *numbers) load(directory string) {
	client := ns.client_pool.Get().(*etcd.Client)
	defer func() {
		ns.client_pool.Put(client)
	}()

	// get the keys under directory
	log.Info("loading numbers from:", directory)
	resp, err := client.Get(directory, true, false)
	if err != nil {
		log.Error(err)
		return
	}

	// validation check
	if !resp.Node.Dir {
		log.Error("not a directory")
		return
	}

	// read all csv(s) from this dirctory
	ns.tables = make(map[string]*table)
	count := 0
	for _, node := range resp.Node.Nodes {
		if !node.Dir {
			log.Trace("loading:", node.Key)
			ns.parse(node.Key, node.Value)
			count++
		} else {
			log.Warning("not a file:", node.Key)
		}
	}

	log.Finef("%v csv(s) loaded", count)
}

// watcher for data change in etcd directory
func (ns *numbers) watcher() {
	client := ns.client_pool.Get().(*etcd.Client)
	defer func() {
		ns.client_pool.Put(client)
	}()

	for {
		ch := make(chan *etcd.Response, 10)
		go func() {
			for {
				if resp, ok := <-ch; ok {
					if !resp.Node.Dir {
						ns.parse(resp.Node.Key, resp.Node.Value)
						log.Trace("csv change:", resp.Node.Key)
					}
				} else {
					return
				}
			}
		}()

		_, err := client.Watch(DEFAULT_NUMBERS_PATH, 0, true, ch, nil)
		if err != nil {
			log.Critical(err)
		}
		<-time.After(RETRY_DELAY)
	}
}

// parse & load a csv
func (ns *numbers) parse(key, value string) {
	ns.Lock()
	defer ns.Unlock()
	src := bytes.NewBufferString(value)
	csv_reader := csv.NewReader(src)
	records, err := csv_reader.ReadAll()
	if err != nil {
		log.Errorf("%v %v", err, key)
		return
	}

	if len(records) == 0 {
		log.Warningf("empty document: %v", key)
		return
	}

	tblname := filepath.Base(key)
	// 记录数据, 第一行为表头，因此从第二行开始
	for line := 1; line < len(records); line++ {
		for field := 1; field < len(records[line]); field++ { // 每条记录的第一个字段作为行索引
			ns.set(ns.tables, tblname, records[line][0], records[0][field], records[line][field])
		}
	}

	// 记录KEYS
	ns.dump_keys(ns.tables, tblname)
}

// set field value
func (ns *numbers) set(tables map[string]*table, tblname string, rowname string, fieldname string, value string) {
	tbl, ok := tables[tblname]
	if !ok {
		tbl = &table{}
		tbl.records = make(map[string]*record)
		tables[tblname] = tbl
	}

	rec, ok := tbl.records[rowname]
	if !ok {
		rec = &record{}
		rec.fields = make(map[string]string)
		tbl.records[rowname] = rec
	}

	rec.fields[fieldname] = value
}

// dump keys
func (ns *numbers) dump_keys(tables map[string]*table, tblname string) {
	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	for k := range tbl.records {
		tbl.keys = append(tbl.keys, k)
	}
}

// get field value
func (ns *numbers) get(tblname string, rowname string, fieldname string) string {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	rec, ok := tbl.records[rowname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " row ", rowname, " not exists!"))
	}

	value, ok := rec.fields[fieldname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " field ", fieldname, " not exists!"))
	}
	return value
}

// get field value as integer
func (ns *numbers) GetInt(tblname string, rowname interface{}, fieldname string) int32 {
	val := ns.get(tblname, fmt.Sprint(rowname), fieldname)
	if val == "" {
		return 0
	}

	v, err := strconv.ParseFloat(val, 64)
	if err != nil {
		panic(fmt.Sprintf("cannot parse integer from gamedata %v %v %v %v\n", tblname, rowname, fieldname, err))
	}

	// round to the integer
	// 1.00000001 -> 1
	// 0.99999999 -> 1
	// -0.9999990 -> -1
	// -1.0000001 -> -1
	return int32(v)
}

// get field value as float
func (ns *numbers) GetFloat(tblname string, rowname interface{}, fieldname string) float64 {
	val := ns.get(tblname, fmt.Sprint(rowname), fieldname)
	if val == "" {
		return 0.0
	}

	f, err := strconv.ParseFloat(val, 64)
	if err != nil {
		panic(fmt.Sprintf("cannot parse float from gamedata %v %v %v %v\n", tblname, rowname, fieldname, err))
	}

	return f
}

// get field value as string
func (ns *numbers) GetString(tblname string, rowname interface{}, fieldname string) string {
	return ns.get(tblname, fmt.Sprint(rowname), fieldname)
}

// get all keys
func (ns *numbers) GetKeys(tblname string) []string {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	return tbl.keys
}

// get row count
func (ns *numbers) Count(tblname string) int32 {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		panic(fmt.Sprint("table ", tblname, " not exists!"))
	}

	return int32(len(tbl.records))
}

// test record exists
func (ns *numbers) IsRecordExists(tblname string, rowname interface{}) bool {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	tbl, ok := tables[tblname]
	if !ok {
		return false
	}

	_, ok = tbl.records[fmt.Sprint(rowname)]
	if !ok {
		return false
	}

	return true
}

// test field exists
func (ns *numbers) IsFieldExists(tblname string, fieldname string) bool {
	ns.RLock()
	defer ns.RUnlock()
	tables := ns.tables

	// check table existence
	tbl, ok := tables[tblname]
	if !ok {
		return false
	}

	// get one record key
	key := ""
	for k, _ := range tbl.records {
		key = k
		break
	}

	rec, ok := tbl.records[key]
	if !ok {
		return false
	}

	_, ok = rec.fields[fieldname]
	if !ok {
		return false
	}

	return true
}

func GetInt(tblname string, rowname interface{}, fieldname string) int32 {
	return _default_numbers.GetInt(tblname, rowname, fieldname)
}

func GetFloat(tblname string, rowname interface{}, fieldname string) float64 {
	return _default_numbers.GetFloat(tblname, rowname, fieldname)
}

func GetString(tblname string, rowname interface{}, fieldname string) string {
	return _default_numbers.GetString(tblname, rowname, fieldname)
}

func GetKeys(tblname string) []string {
	return _default_numbers.GetKeys(tblname)
}

func Count(tblname string) int32 {
	return _default_numbers.Count(tblname)
}

func IsFieldExists(tblname string, fieldname string) bool {
	return _default_numbers.IsFieldExists(tblname, fieldname)
}

func IsRecordExists(tblname string, rowname interface{}) bool {
	return _default_numbers.IsRecordExists(tblname, rowname)
}
<|endoftext|>


<commit_before>// Copyright 2014 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package orchestrator

import (
	"github.com/andreaskoch/allmark2/common/config"
	"github.com/andreaskoch/allmark2/common/logger"
	"github.com/andreaskoch/allmark2/common/paths"
	"github.com/andreaskoch/allmark2/common/route"
	"github.com/andreaskoch/allmark2/dataaccess"
	"github.com/andreaskoch/allmark2/model"
	"github.com/andreaskoch/allmark2/services/converter"
	"github.com/andreaskoch/allmark2/services/parser"
	"github.com/andreaskoch/allmark2/web/orchestrator/index"
	"github.com/andreaskoch/allmark2/web/orchestrator/search"
	"github.com/andreaskoch/allmark2/web/view/viewmodel"
	"github.com/andreaskoch/allmark2/web/webpaths"
	"strings"
	"time"
)

type CacheState int

const (
	CacheStateStale CacheState = iota
	CacheStatePrimed
)

func newBaseOrchestrator(logger logger.Logger, config config.Config, repository dataaccess.Repository, parser parser.Parser, converter converter.Converter, webPathProvider webpaths.WebPathProvider) *Orchestrator {
	orchestrator := &Orchestrator{
		logger: logger,

		config:     config,
		repository: repository,
		parser:     parser,
		converter:  converter,

		webPathProvider: webPathProvider,
	}

	// warm up caches
	orchestrator.blockingCacheWarmup()

	return orchestrator
}

type Orchestrator struct {
	logger logger.Logger

	config     config.Config
	repository dataaccess.Repository
	parser     parser.Parser
	converter  converter.Converter

	webPathProvider webpaths.WebPathProvider

	// cache control
	cacheStatusMap    map[string]CacheState
	cachePrimerMap    map[string]func()
	cachePrimerStatus map[string]bool

	// caches and indizes
	fulltextIndex   *search.ItemSearch
	repositoryIndex *index.Index
	items           []*model.Item
	itemsByAlias    map[string]*model.Item
}

func (orchestrator *Orchestrator) blockingCacheWarmup() {
	orchestrator.index()
	orchestrator.getAllItems()
	orchestrator.search("", 5)
	orchestrator.getItemByAlias("")

	orchestrator.primeCaches()
}

// Reset all Caches
func (orchestrator *Orchestrator) ResetCache() {

	// mark all caches as stale
	for cacheType := range orchestrator.cacheStatusMap {
		orchestrator.cacheStatusMap[cacheType] = CacheStateStale
	}

	// prime all caches asynchronously
	go func() {
		orchestrator.primeCaches()
	}()

}

func (orchestrator *Orchestrator) setCache(cacheType string, primer func()) {

	// initialize the primer map on first use
	if orchestrator.cachePrimerMap == nil {
		orchestrator.cachePrimerMap = make(map[string]func())
	}

	// initialize the status map on first use
	if orchestrator.cacheStatusMap == nil {
		orchestrator.cacheStatusMap = make(map[string]CacheState)
	}

	// store the primer
	orchestrator.cachePrimerMap[cacheType] = primer

	// fill the cache
	primer()

	// mark the cache type as primed
	orchestrator.cacheStatusMap[cacheType] = CacheStatePrimed
}

func (orchestrator *Orchestrator) isCacheStale(cacheType string) bool {
	if status, exists := orchestrator.cacheStatusMap[cacheType]; exists {
		return status == CacheStateStale
	}

	// if there is no status it is definitly stale
	return true
}

// Prime all caches
func (orchestrator *Orchestrator) primeCaches() {
	for cacheType := range orchestrator.cacheStatusMap {
		orchestrator.primeCache(cacheType)
	}
}

// Prime a particular cache
func (orchestrator *Orchestrator) primeCache(cacheType string) {

	// initialize the mutex map
	if orchestrator.cachePrimerStatus == nil {
		orchestrator.cachePrimerStatus = make(map[string]bool)
	}

	// check if there is a mutex
	if exists, _ := orchestrator.cachePrimerStatus[cacheType]; exists {

		// abort. There is already a primer running for the supplied cache type
		return
	}

	// set a mutex for the supplied cache type
	orchestrator.cachePrimerStatus[cacheType] = true

	// execute the primer func
	primerFunc := orchestrator.cachePrimerMap[cacheType]
	primerFunc()

	// set the cache status to "primed"
	orchestrator.cacheStatusMap[cacheType] = CacheStatePrimed

	// release the mutex
	defer delete(orchestrator.cachePrimerStatus, cacheType)
}

func (orchestrator *Orchestrator) ItemExists(route route.Route) bool {
	_, exists := orchestrator.index().IsMatch(route)
	return exists
}

func (orchestrator *Orchestrator) absolutePather(prefix string) paths.Pather {
	return orchestrator.webPathProvider.AbsolutePather(prefix)
}

func (orchestrator *Orchestrator) itemPather() paths.Pather {
	return orchestrator.webPathProvider.ItemPather()
}

func (orchestrator *Orchestrator) tagPather() paths.Pather {
	return orchestrator.webPathProvider.TagPather()
}

func (orchestrator *Orchestrator) relativePather(baseRoute route.Route) paths.Pather {
	return orchestrator.webPathProvider.RelativePather(baseRoute)
}

func (orchestrator *Orchestrator) parseItem(item *dataaccess.Item) *model.Item {
	parsedItem, err := orchestrator.parser.ParseItem(item)
	if err != nil {
		orchestrator.logger.Warn(err.Error())
		return nil
	}

	return parsedItem
}

func (orchestrator *Orchestrator) parseFile(file *dataaccess.File) *model.File {
	parsedFile, err := orchestrator.parser.ParseFile(file)
	if err != nil {
		orchestrator.logger.Warn(err.Error())
		return nil
	}

	return parsedFile
}

func (orchestrator *Orchestrator) rootItem() *model.Item {
	return orchestrator.index().Root()
}

func (orchestrator *Orchestrator) getItem(route route.Route) *model.Item {

	if item, exists := orchestrator.index().IsMatch(route); exists {
		return item
	}

	return nil
}

func (orchestrator *Orchestrator) getLatestItems(parentRoute route.Route) []*model.Item {

	leafes := orchestrator.index().GetLeafes(parentRoute)

	// sort the leafes by date
	model.SortItemsBy(sortItemsByDate).Sort(leafes)

	return leafes
}

func (orchestrator *Orchestrator) index() *index.Index {

	cacheType := "index"

	// load from cache
	if orchestrator.repositoryIndex != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.repositoryIndex
	}

	orchestrator.setCache(cacheType, func() {

		// parse all items
		repositoryItems := orchestrator.repository.Items()
		parsedItems := make([]*model.Item, 0, len(repositoryItems))
		for _, repositoryItem := range repositoryItems {
			parsedItem := orchestrator.parseItem(repositoryItem)
			if parsedItem == nil {
				continue
			}

			parsedItems = append(parsedItems, parsedItem)
		}

		// create a new index
		newIndex := index.New(orchestrator.logger, parsedItems)

		// store to cache
		orchestrator.repositoryIndex = newIndex
	})

	return orchestrator.repositoryIndex
}

func (orchestrator *Orchestrator) search(keywords string, maxiumNumberOfResults int) []search.Result {
	cacheType := "fulltextIndex"

	// load from cache
	if orchestrator.fulltextIndex != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.fulltextIndex.Search(keywords, maxiumNumberOfResults)
	}

	orchestrator.setCache(cacheType, func() {

		newFullTextIndex := search.NewItemSearch(orchestrator.logger, orchestrator.getAllItems())

		// destroy the old index
		if orchestrator.fulltextIndex != nil {
			oldIndex := orchestrator.fulltextIndex
			go oldIndex.Destroy()
		}

		// store to cache
		orchestrator.fulltextIndex = newFullTextIndex
	})

	return orchestrator.fulltextIndex.Search(keywords, maxiumNumberOfResults)
}

func (orchestrator *Orchestrator) getAllItems() []*model.Item {

	cacheType := "allItems"

	// load from cache
	if orchestrator.items != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.items
	}

	orchestrator.setCache(cacheType, func() {

		// get all items
		allItems := orchestrator.index().GetAllItems()

		// sort the items by date
		model.SortItemsBy(sortItemsByDate).Sort(allItems)

		// store to cache
		orchestrator.items = allItems
	})

	return orchestrator.items
}

func (orchestrator *Orchestrator) getItems(pageSize, page int) []*model.Item {

	allItems := orchestrator.getAllItems()

	// determine the start index
	startIndex := pageSize * (page - 1)
	if startIndex >= len(allItems) {
		return []*model.Item{}
	}

	// determine the end index
	endIndex := startIndex + pageSize
	if endIndex > len(allItems) {
		endIndex = len(allItems)
	}

	return allItems[startIndex:endIndex]
}

func (orchestrator *Orchestrator) getCreationDate(itemRoute route.Route) (creationDate time.Time, found bool) {

	item := orchestrator.getItem(itemRoute)
	if item == nil {
		return time.Time{}, false
	}

	return item.MetaData.CreationDate, true
}

func (orchestrator *Orchestrator) getFile(route route.Route) *model.File {
	file, exists := orchestrator.index().IsFileMatch(route)
	if !exists {
		return nil
	}

	return file
}

func (orchestrator *Orchestrator) getParent(route route.Route) *model.Item {
	parent := orchestrator.index().GetParent(route)
	if parent == nil {
		return nil
	}

	return parent
}

func (orchestrator *Orchestrator) getPrevious(currentRoute route.Route) *model.Item {

	latestItems := orchestrator.getLatestItems(route.New())
	if len(latestItems) == 0 {
		return nil
	}

	// determine the position of the supplied route
	matchingIndex := -1
	for index, item := range latestItems {
		if item.Route().Value() == currentRoute.Value() {
			matchingIndex = index
			break
		}
	}

	// abort if the route was not found at all
	if noMatchFound := (matchingIndex == -1); noMatchFound {
		return nil
	}

	// abort if there is no next item
	nextIndex := matchingIndex + 1
	if nextIndex >= len(latestItems) {
		return nil
	}

	return latestItems[nextIndex]
}

func (orchestrator *Orchestrator) getNext(currentRoute route.Route) *model.Item {

	latestItems := orchestrator.getLatestItems(route.New())
	if len(latestItems) == 0 {
		return nil
	}

	// determine the position of the supplied route
	matchingIndex := -1
	for index, item := range latestItems {
		if item.Route().Value() == currentRoute.Value() {
			matchingIndex = index
			break
		}
	}

	// abort if the route was not found at all
	if noMatchFound := (matchingIndex == -1); noMatchFound {
		return nil
	}

	// abort if there is no previous item
	previousIndex := matchingIndex - 1
	if noPreviousItem := (previousIndex < 0); noPreviousItem {
		return nil
	}

	return latestItems[previousIndex]
}

func (orchestrator *Orchestrator) getChilds(route route.Route) []*model.Item {

	// get all childs
	childs := orchestrator.index().GetDirectChilds(route)

	// sort the childs by date
	model.SortItemsBy(sortItemsByDate).Sort(childs)

	return childs
}

// Get the item that has the specified alias. Returns nil if there is no matching item.
func (orchestrator *Orchestrator) getItemByAlias(alias string) *model.Item {

	cacheType := "itembyalias"
	alias = strings.TrimSpace(strings.ToLower(alias))

	// load from cache
	if orchestrator.itemsByAlias != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.itemsByAlias[alias]
	}

	orchestrator.setCache(cacheType, func() {

		itemsByAlias := make(map[string]*model.Item)

		for _, item := range orchestrator.getAllItems() {

			// continue items without an alias
			if item.MetaData.Alias == "" {
				continue
			}

			itemAlias := strings.TrimSpace(strings.ToLower(item.MetaData.Alias))
			itemsByAlias[itemAlias] = item
		}

		orchestrator.itemsByAlias = itemsByAlias
	})

	return orchestrator.itemsByAlias[alias]
}

func (orchestrator *Orchestrator) getAnalyticsSettings() viewmodel.Analytics {
	return viewmodel.Analytics{
		Enabled: orchestrator.config.Analytics.Enabled,
		GoogleAnalytics: viewmodel.GoogleAnalytics{
			Enabled:    orchestrator.config.Analytics.GoogleAnalytics.Enabled,
			TrackingId: orchestrator.config.Analytics.GoogleAnalytics.TrackingId,
		},
	}
}
<commit_msg>fix. don't call prime caches twice<commit_after>// Copyright 2014 Andreas Koch. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package orchestrator

import (
	"github.com/andreaskoch/allmark2/common/config"
	"github.com/andreaskoch/allmark2/common/logger"
	"github.com/andreaskoch/allmark2/common/paths"
	"github.com/andreaskoch/allmark2/common/route"
	"github.com/andreaskoch/allmark2/dataaccess"
	"github.com/andreaskoch/allmark2/model"
	"github.com/andreaskoch/allmark2/services/converter"
	"github.com/andreaskoch/allmark2/services/parser"
	"github.com/andreaskoch/allmark2/web/orchestrator/index"
	"github.com/andreaskoch/allmark2/web/orchestrator/search"
	"github.com/andreaskoch/allmark2/web/view/viewmodel"
	"github.com/andreaskoch/allmark2/web/webpaths"
	"strings"
	"time"
)

type CacheState int

const (
	CacheStateStale CacheState = iota
	CacheStatePrimed
)

func newBaseOrchestrator(logger logger.Logger, config config.Config, repository dataaccess.Repository, parser parser.Parser, converter converter.Converter, webPathProvider webpaths.WebPathProvider) *Orchestrator {
	orchestrator := &Orchestrator{
		logger: logger,

		config:     config,
		repository: repository,
		parser:     parser,
		converter:  converter,

		webPathProvider: webPathProvider,
	}

	// warm up caches
	orchestrator.blockingCacheWarmup()

	return orchestrator
}

type Orchestrator struct {
	logger logger.Logger

	config     config.Config
	repository dataaccess.Repository
	parser     parser.Parser
	converter  converter.Converter

	webPathProvider webpaths.WebPathProvider

	// cache control
	cacheStatusMap    map[string]CacheState
	cachePrimerMap    map[string]func()
	cachePrimerStatus map[string]bool

	// caches and indizes
	fulltextIndex   *search.ItemSearch
	repositoryIndex *index.Index
	items           []*model.Item
	itemsByAlias    map[string]*model.Item
}

func (orchestrator *Orchestrator) blockingCacheWarmup() {
	orchestrator.index()
	orchestrator.getAllItems()
	orchestrator.search("", 5)
	orchestrator.getItemByAlias("")
}

// Reset all Caches
func (orchestrator *Orchestrator) ResetCache() {

	// mark all caches as stale
	for cacheType := range orchestrator.cacheStatusMap {
		orchestrator.cacheStatusMap[cacheType] = CacheStateStale
	}

	// prime all caches asynchronously
	go func() {
		orchestrator.primeCaches()
	}()

}

func (orchestrator *Orchestrator) setCache(cacheType string, primer func()) {

	// initialize the primer map on first use
	if orchestrator.cachePrimerMap == nil {
		orchestrator.cachePrimerMap = make(map[string]func())
	}

	// initialize the status map on first use
	if orchestrator.cacheStatusMap == nil {
		orchestrator.cacheStatusMap = make(map[string]CacheState)
	}

	// store the primer
	orchestrator.cachePrimerMap[cacheType] = primer

	// fill the cache
	primer()

	// mark the cache type as primed
	orchestrator.cacheStatusMap[cacheType] = CacheStatePrimed
}

func (orchestrator *Orchestrator) isCacheStale(cacheType string) bool {
	if status, exists := orchestrator.cacheStatusMap[cacheType]; exists {
		return status == CacheStateStale
	}

	// if there is no status it is definitly stale
	return true
}

// Prime all caches
func (orchestrator *Orchestrator) primeCaches() {
	for cacheType := range orchestrator.cacheStatusMap {
		orchestrator.primeCache(cacheType)
	}
}

// Prime a particular cache
func (orchestrator *Orchestrator) primeCache(cacheType string) {

	// initialize the mutex map
	if orchestrator.cachePrimerStatus == nil {
		orchestrator.cachePrimerStatus = make(map[string]bool)
	}

	// check if there is a mutex
	if exists, _ := orchestrator.cachePrimerStatus[cacheType]; exists {

		// abort. There is already a primer running for the supplied cache type
		return
	}

	// set a mutex for the supplied cache type
	orchestrator.cachePrimerStatus[cacheType] = true

	// execute the primer func
	primerFunc := orchestrator.cachePrimerMap[cacheType]
	primerFunc()

	// set the cache status to "primed"
	orchestrator.cacheStatusMap[cacheType] = CacheStatePrimed

	// release the mutex
	defer delete(orchestrator.cachePrimerStatus, cacheType)
}

func (orchestrator *Orchestrator) ItemExists(route route.Route) bool {
	_, exists := orchestrator.index().IsMatch(route)
	return exists
}

func (orchestrator *Orchestrator) absolutePather(prefix string) paths.Pather {
	return orchestrator.webPathProvider.AbsolutePather(prefix)
}

func (orchestrator *Orchestrator) itemPather() paths.Pather {
	return orchestrator.webPathProvider.ItemPather()
}

func (orchestrator *Orchestrator) tagPather() paths.Pather {
	return orchestrator.webPathProvider.TagPather()
}

func (orchestrator *Orchestrator) relativePather(baseRoute route.Route) paths.Pather {
	return orchestrator.webPathProvider.RelativePather(baseRoute)
}

func (orchestrator *Orchestrator) parseItem(item *dataaccess.Item) *model.Item {
	parsedItem, err := orchestrator.parser.ParseItem(item)
	if err != nil {
		orchestrator.logger.Warn(err.Error())
		return nil
	}

	return parsedItem
}

func (orchestrator *Orchestrator) parseFile(file *dataaccess.File) *model.File {
	parsedFile, err := orchestrator.parser.ParseFile(file)
	if err != nil {
		orchestrator.logger.Warn(err.Error())
		return nil
	}

	return parsedFile
}

func (orchestrator *Orchestrator) rootItem() *model.Item {
	return orchestrator.index().Root()
}

func (orchestrator *Orchestrator) getItem(route route.Route) *model.Item {

	if item, exists := orchestrator.index().IsMatch(route); exists {
		return item
	}

	return nil
}

func (orchestrator *Orchestrator) getLatestItems(parentRoute route.Route) []*model.Item {

	leafes := orchestrator.index().GetLeafes(parentRoute)

	// sort the leafes by date
	model.SortItemsBy(sortItemsByDate).Sort(leafes)

	return leafes
}

func (orchestrator *Orchestrator) index() *index.Index {

	cacheType := "index"

	// load from cache
	if orchestrator.repositoryIndex != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.repositoryIndex
	}

	orchestrator.setCache(cacheType, func() {

		// parse all items
		repositoryItems := orchestrator.repository.Items()
		parsedItems := make([]*model.Item, 0, len(repositoryItems))
		for _, repositoryItem := range repositoryItems {
			parsedItem := orchestrator.parseItem(repositoryItem)
			if parsedItem == nil {
				continue
			}

			parsedItems = append(parsedItems, parsedItem)
		}

		// create a new index
		newIndex := index.New(orchestrator.logger, parsedItems)

		// store to cache
		orchestrator.repositoryIndex = newIndex
	})

	return orchestrator.repositoryIndex
}

func (orchestrator *Orchestrator) search(keywords string, maxiumNumberOfResults int) []search.Result {
	cacheType := "fulltextIndex"

	// load from cache
	if orchestrator.fulltextIndex != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.fulltextIndex.Search(keywords, maxiumNumberOfResults)
	}

	orchestrator.setCache(cacheType, func() {

		newFullTextIndex := search.NewItemSearch(orchestrator.logger, orchestrator.getAllItems())

		// destroy the old index
		if orchestrator.fulltextIndex != nil {
			oldIndex := orchestrator.fulltextIndex
			go oldIndex.Destroy()
		}

		// store to cache
		orchestrator.fulltextIndex = newFullTextIndex
	})

	return orchestrator.fulltextIndex.Search(keywords, maxiumNumberOfResults)
}

func (orchestrator *Orchestrator) getAllItems() []*model.Item {

	cacheType := "allItems"

	// load from cache
	if orchestrator.items != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.items
	}

	orchestrator.setCache(cacheType, func() {

		// get all items
		allItems := orchestrator.index().GetAllItems()

		// sort the items by date
		model.SortItemsBy(sortItemsByDate).Sort(allItems)

		// store to cache
		orchestrator.items = allItems
	})

	return orchestrator.items
}

func (orchestrator *Orchestrator) getItems(pageSize, page int) []*model.Item {

	allItems := orchestrator.getAllItems()

	// determine the start index
	startIndex := pageSize * (page - 1)
	if startIndex >= len(allItems) {
		return []*model.Item{}
	}

	// determine the end index
	endIndex := startIndex + pageSize
	if endIndex > len(allItems) {
		endIndex = len(allItems)
	}

	return allItems[startIndex:endIndex]
}

func (orchestrator *Orchestrator) getCreationDate(itemRoute route.Route) (creationDate time.Time, found bool) {

	item := orchestrator.getItem(itemRoute)
	if item == nil {
		return time.Time{}, false
	}

	return item.MetaData.CreationDate, true
}

func (orchestrator *Orchestrator) getFile(route route.Route) *model.File {
	file, exists := orchestrator.index().IsFileMatch(route)
	if !exists {
		return nil
	}

	return file
}

func (orchestrator *Orchestrator) getParent(route route.Route) *model.Item {
	parent := orchestrator.index().GetParent(route)
	if parent == nil {
		return nil
	}

	return parent
}

func (orchestrator *Orchestrator) getPrevious(currentRoute route.Route) *model.Item {

	latestItems := orchestrator.getLatestItems(route.New())
	if len(latestItems) == 0 {
		return nil
	}

	// determine the position of the supplied route
	matchingIndex := -1
	for index, item := range latestItems {
		if item.Route().Value() == currentRoute.Value() {
			matchingIndex = index
			break
		}
	}

	// abort if the route was not found at all
	if noMatchFound := (matchingIndex == -1); noMatchFound {
		return nil
	}

	// abort if there is no next item
	nextIndex := matchingIndex + 1
	if nextIndex >= len(latestItems) {
		return nil
	}

	return latestItems[nextIndex]
}

func (orchestrator *Orchestrator) getNext(currentRoute route.Route) *model.Item {

	latestItems := orchestrator.getLatestItems(route.New())
	if len(latestItems) == 0 {
		return nil
	}

	// determine the position of the supplied route
	matchingIndex := -1
	for index, item := range latestItems {
		if item.Route().Value() == currentRoute.Value() {
			matchingIndex = index
			break
		}
	}

	// abort if the route was not found at all
	if noMatchFound := (matchingIndex == -1); noMatchFound {
		return nil
	}

	// abort if there is no previous item
	previousIndex := matchingIndex - 1
	if noPreviousItem := (previousIndex < 0); noPreviousItem {
		return nil
	}

	return latestItems[previousIndex]
}

func (orchestrator *Orchestrator) getChilds(route route.Route) []*model.Item {

	// get all childs
	childs := orchestrator.index().GetDirectChilds(route)

	// sort the childs by date
	model.SortItemsBy(sortItemsByDate).Sort(childs)

	return childs
}

// Get the item that has the specified alias. Returns nil if there is no matching item.
func (orchestrator *Orchestrator) getItemByAlias(alias string) *model.Item {

	cacheType := "itembyalias"
	alias = strings.TrimSpace(strings.ToLower(alias))

	// load from cache
	if orchestrator.itemsByAlias != nil {

		// re-prime the cache if it is stale
		if orchestrator.isCacheStale(cacheType) {
			go orchestrator.primeCache(cacheType)
		}

		return orchestrator.itemsByAlias[alias]
	}

	orchestrator.setCache(cacheType, func() {

		itemsByAlias := make(map[string]*model.Item)

		for _, item := range orchestrator.getAllItems() {

			// continue items without an alias
			if item.MetaData.Alias == "" {
				continue
			}

			itemAlias := strings.TrimSpace(strings.ToLower(item.MetaData.Alias))
			itemsByAlias[itemAlias] = item
		}

		orchestrator.itemsByAlias = itemsByAlias
	})

	return orchestrator.itemsByAlias[alias]
}

func (orchestrator *Orchestrator) getAnalyticsSettings() viewmodel.Analytics {
	return viewmodel.Analytics{
		Enabled: orchestrator.config.Analytics.Enabled,
		GoogleAnalytics: viewmodel.GoogleAnalytics{
			Enabled:    orchestrator.config.Analytics.GoogleAnalytics.Enabled,
			TrackingId: orchestrator.config.Analytics.GoogleAnalytics.TrackingId,
		},
	}
}
<|endoftext|>


<commit_before><commit_msg>encoding/json: improve omitempty test coverage<commit_after><|endoftext|>


<commit_before>package fire

import (
	"fmt"
	"time"

	"github.com/labstack/echo"
	"github.com/labstack/echo/engine"
	"github.com/labstack/echo/engine/standard"
	"github.com/labstack/echo/middleware"
	"gopkg.in/mgo.v2"
)

// An Application provides an out-of-the-box configuration of components to
// get started with building JSON APIs.
type Application struct {
	set    *Set
	router *echo.Echo

	bodyLimit      string
	allowedOrigins []string
	allowedHeaders []string

	disableCORS            bool
	disableCompression     bool
	disableRecovery        bool
	disableCommonSecurity  bool
	enableMethodOverriding bool
	enableDevMode          bool
}

// New creates and returns a new Application.
func New(mongoURI, prefix string) *Application {
	// create router
	router := echo.New()

	// connect to database
	sess, err := mgo.Dial(mongoURI)
	if err != nil {
		panic(err)
	}

	// create controller set
	set := NewSet(sess, router, prefix)

	return &Application{
		set:            set,
		router:         router,
		bodyLimit:      "4K",
		allowedOrigins: []string{"*"},
		allowedHeaders: []string{
			echo.HeaderOrigin,
			echo.HeaderContentType,
			echo.HeaderAuthorization,
		},
	}
}

// Mount will add controllers to the set and register them on the router.
//
// Note: Each controller should only be mounted once.
func (a *Application) Mount(controllers ...*Controller) {
	a.set.Mount(controllers...)
}

// Router will return the internally used echo instance.
func (a *Application) Router() *echo.Echo {
	return a.router
}

// EnableSecurity will enable further security measures for your application.
func (a *Application) EnableSecurity() {
	a.router.Use()
}

// EnableMethodOverriding will enable the usage of the X-HTTP-Method-Override
// header to set a request method when using the POST method.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) EnableMethodOverriding() {
	a.enableMethodOverriding = true
}

// SetBodyLimit can be used to override the default body limit of 4K with a new
// value in the form of 4K, 2M, 1G or 1P.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) SetBodyLimit(size string) {
	a.bodyLimit = size
}

// SetAllowedOrigins will replace the default allowed origin set `*`.
func (a *Application) SetAllowedOrigins(origins ...string) {
	a.allowedOrigins = origins
}

// AddAllowedHeaders will allow additional headers.
func (a *Application) AddAllowedHeaders(headers ...string) {
	a.allowedHeaders = append(a.allowedHeaders, headers)
}

// DisableCORS will turn off CORS support.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableCORS(origins ...string) {
	a.disableCORS = true
}

// DisableCompression will turn of gzip compression.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableCompression() {
	a.disableCompression = true
}

// DisableRecovery will disable the automatic recover mechanism.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableRecovery() {
	a.disableRecovery = true
}

// DisableCommonSecurity will disable common security features including:
// protection against cross-site scripting attacks by setting the
// `X-XSS-Protection` header, protection against overriding Content-Type
// header by setting the `X-Content-Type-Options` header and protection against
// clickjacking by setting the `X-Frame-Options` header.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableCommonSecurity() {
	a.disableCommonSecurity = true
}

// EnableDevMode will enable the development mode that prints all registered
// handlers on boot and all incoming requests.
func (a *Application) EnableDevMode() {
	a.enableDevMode = true
}

// Run will run the application using the passed server.
func (a *Application) Run(server engine.Server) {
	// set body limit
	a.router.Use(middleware.BodyLimit(a.bodyLimit))

	// enable cors
	if !a.disableCORS {
		allowedHeaders := a.allowedHeaders

		// add method override header if enabled
		if a.enableMethodOverriding {
			allowedHeaders = append(allowedHeaders, echo.HeaderXHTTPMethodOverride)
		}

		// add cors middleware
		a.router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
			AllowOrigins: a.allowedOrigins,
			AllowMethods: []string{echo.GET, echo.POST, echo.PATCH, echo.DELETE},
			AllowHeaders: allowedHeaders,
			MaxAge:       60,
		}))
	}

	// enable gzip compression
	if !a.disableCompression {
		a.router.Use(middleware.Gzip())
	}

	// enable automatic recovery
	if !a.disableRecovery {
		a.router.Use(middleware.Recover())
	}

	// enable common security
	if !a.disableCommonSecurity {
		a.router.Use(middleware.Secure())
	}

	// enable method overriding
	if a.enableMethodOverriding {
		a.router.Pre(middleware.MethodOverride())
	}

	// enable dev mode
	if a.enableDevMode {
		a.printInfo()
		a.router.Use(a.logger)
	}

	a.router.Run(server)
}

// Start will run the application on the specified address.
func (a *Application) Start(addr string) {
	a.Run(standard.New(addr))
}

func (a *Application) printInfo() {
	fmt.Println("==> Fire application starting...")
	fmt.Println("==> Registered routes:")

	for _, route := range a.router.Routes() {
		fmt.Printf("%6s  %-30s  %s\n", route.Method, route.Path, route.Handler)
	}

	fmt.Println("==> Ready to go!")
}

func (a *Application) logger(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) (err error) {
		req := c.Request()
		res := c.Response()

		start := time.Now()
		if err = next(c); err != nil {
			c.Error(err)
		}

		duration := time.Since(start).String()

		path := req.URL().Path()
		if path == "" {
			path = "/"
		}

		fmt.Printf("%6s  %-30s  %d  %s\n", req.Method(), path, res.Status(), duration)

		return
	}
}
<commit_msg>cleanup<commit_after>package fire

import (
	"fmt"
	"time"

	"github.com/labstack/echo"
	"github.com/labstack/echo/engine"
	"github.com/labstack/echo/engine/standard"
	"github.com/labstack/echo/middleware"
	"gopkg.in/mgo.v2"
)

// An Application provides an out-of-the-box configuration of components to
// get started with building JSON APIs.
type Application struct {
	set    *Set
	router *echo.Echo

	bodyLimit      string
	allowedOrigins []string
	allowedHeaders []string

	disableCORS            bool
	disableCompression     bool
	disableRecovery        bool
	disableCommonSecurity  bool
	enableMethodOverriding bool
	enableDevMode          bool
}

// New creates and returns a new Application.
func New(mongoURI, prefix string) *Application {
	// create router
	router := echo.New()

	// connect to database
	sess, err := mgo.Dial(mongoURI)
	if err != nil {
		panic(err)
	}

	// create controller set
	set := NewSet(sess, router, prefix)

	return &Application{
		set:            set,
		router:         router,
		bodyLimit:      "4K",
		allowedOrigins: []string{"*"},
		allowedHeaders: []string{
			echo.HeaderOrigin,
			echo.HeaderContentType,
			echo.HeaderAuthorization,
		},
	}
}

// Mount will add controllers to the set and register them on the router.
//
// Note: Each controller should only be mounted once.
func (a *Application) Mount(controllers ...*Controller) {
	a.set.Mount(controllers...)
}

// Router will return the internally used echo instance.
func (a *Application) Router() *echo.Echo {
	return a.router
}

// EnableMethodOverriding will enable the usage of the X-HTTP-Method-Override
// header to set a request method when using the POST method.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) EnableMethodOverriding() {
	a.enableMethodOverriding = true
}

// SetBodyLimit can be used to override the default body limit of 4K with a new
// value in the form of 4K, 2M, 1G or 1P.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) SetBodyLimit(size string) {
	a.bodyLimit = size
}

// SetAllowedOrigins will replace the default allowed origin set `*`.
func (a *Application) SetAllowedOrigins(origins ...string) {
	a.allowedOrigins = origins
}

// AddAllowedHeaders will allow additional headers.
func (a *Application) AddAllowedHeaders(headers ...string) {
	a.allowedHeaders = append(a.allowedHeaders, headers)
}

// DisableCORS will turn off CORS support.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableCORS(origins ...string) {
	a.disableCORS = true
}

// DisableCompression will turn of gzip compression.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableCompression() {
	a.disableCompression = true
}

// DisableRecovery will disable the automatic recover mechanism.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableRecovery() {
	a.disableRecovery = true
}

// DisableCommonSecurity will disable common security features including:
// protection against cross-site scripting attacks by setting the
// `X-XSS-Protection` header, protection against overriding Content-Type
// header by setting the `X-Content-Type-Options` header and protection against
// clickjacking by setting the `X-Frame-Options` header.
//
// Note: This method must be called before calling Run or Start.
func (a *Application) DisableCommonSecurity() {
	a.disableCommonSecurity = true
}

// EnableDevMode will enable the development mode that prints all registered
// handlers on boot and all incoming requests.
func (a *Application) EnableDevMode() {
	a.enableDevMode = true
}

// Run will run the application using the passed server.
func (a *Application) Run(server engine.Server) {
	// set body limit
	a.router.Use(middleware.BodyLimit(a.bodyLimit))

	// enable cors
	if !a.disableCORS {
		allowedHeaders := a.allowedHeaders

		// add method override header if enabled
		if a.enableMethodOverriding {
			allowedHeaders = append(allowedHeaders, echo.HeaderXHTTPMethodOverride)
		}

		// add cors middleware
		a.router.Use(middleware.CORSWithConfig(middleware.CORSConfig{
			AllowOrigins: a.allowedOrigins,
			AllowMethods: []string{echo.GET, echo.POST, echo.PATCH, echo.DELETE},
			AllowHeaders: allowedHeaders,
			MaxAge:       60,
		}))
	}

	// enable gzip compression
	if !a.disableCompression {
		a.router.Use(middleware.Gzip())
	}

	// enable automatic recovery
	if !a.disableRecovery {
		a.router.Use(middleware.Recover())
	}

	// enable common security
	if !a.disableCommonSecurity {
		a.router.Use(middleware.Secure())
	}

	// enable method overriding
	if a.enableMethodOverriding {
		a.router.Pre(middleware.MethodOverride())
	}

	// enable dev mode
	if a.enableDevMode {
		a.printInfo()
		a.router.Use(a.logger)
	}

	a.router.Run(server)
}

// Start will run the application on the specified address.
func (a *Application) Start(addr string) {
	a.Run(standard.New(addr))
}

func (a *Application) printInfo() {
	fmt.Println("==> Fire application starting...")
	fmt.Println("==> Registered routes:")

	for _, route := range a.router.Routes() {
		fmt.Printf("%6s  %-30s  %s\n", route.Method, route.Path, route.Handler)
	}

	fmt.Println("==> Ready to go!")
}

func (a *Application) logger(next echo.HandlerFunc) echo.HandlerFunc {
	return func(c echo.Context) (err error) {
		req := c.Request()
		res := c.Response()

		start := time.Now()
		if err = next(c); err != nil {
			c.Error(err)
		}

		duration := time.Since(start).String()

		path := req.URL().Path()
		if path == "" {
			path = "/"
		}

		fmt.Printf("%6s  %-30s  %d  %s\n", req.Method(), path, res.Status(), duration)

		return
	}
}
<|endoftext|>