text:
package repo

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"math"
	"math/big"
	"os"
	"path"
	"regexp"
	"strconv"
	"strings"
	"time"
	"unicode/utf8"

	cid "gx/ipfs/QmTbxNB1NwDesLmKTscr4udL2tVP7MaxvXnD1D9yX7g3PN/go-cid"
	mh "gx/ipfs/QmerPMzPk1mJVowm8KgmoknWa4yCYvvugMPsgWmDNUvDLW/go-multihash"

	"github.com/OpenBazaar/jsonpb"
	"github.com/OpenBazaar/openbazaar-go/ipfs"
	"github.com/OpenBazaar/openbazaar-go/pb"
	"github.com/OpenBazaar/openbazaar-go/util"
	"github.com/golang/protobuf/proto"
	"github.com/gosimple/slug"
	"github.com/microcosm-cc/bluemonday"
)

const (
	// ListingVersion - current listing version
	ListingVersion = 5
	// TitleMaxCharacters - max size for title
	TitleMaxCharacters = 140
	// ShortDescriptionLength - min length for description
	ShortDescriptionLength = 160
	// DescriptionMaxCharacters - max length for description
	DescriptionMaxCharacters = 50000
	// MaxTags - max permitted tags
	MaxTags = 10
	// MaxCategories - max permitted categories
	MaxCategories = 10
	// MaxListItems - max items in a listing
	MaxListItems = 30
	// FilenameMaxCharacters - max filename size
	FilenameMaxCharacters = 255
	// CodeMaxCharacters - max chars for a code
	CodeMaxCharacters = 20
	// WordMaxCharacters - max chars for word
	WordMaxCharacters = 40
	// SentenceMaxCharacters - max chars for sentence
	SentenceMaxCharacters = 70
	// CouponTitleMaxCharacters - max length of a coupon title
	CouponTitleMaxCharacters = 70
	// PolicyMaxCharacters - max length for policy
	PolicyMaxCharacters = 10000
	// AboutMaxCharacters - max length for about
	AboutMaxCharacters = 10000
	// URLMaxCharacters - max length for URL
	URLMaxCharacters = 2000
	// MaxCountryCodes - max country codes
	MaxCountryCodes = 255
	// DefaultEscrowTimeout - escrow timeout in hours
	DefaultEscrowTimeout = 1080
	// SlugBuffer - buffer size for slug
	SlugBuffer = 5
	// PriceModifierMin - min price modifier
	PriceModifierMin = -99.99
	// PriceModifierMax = max price modifier
	PriceModifierMax = 1000.00
)

type option struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}
type options []option

func (os options) ToOrderOptionSetProtobuf() []*pb.Order_Item_Option {
	var optionProtos = make([]*pb.Order_Item_Option, len(os))
	for i, o := range os {
		optionProtos[i] = &pb.Order_Item_Option{
			Name:  o.Name,
			Value: o.Value,
		}
	}
	return optionProtos
}

type shippingOption struct {
	Name    string `json:"name"`
	Service string `json:"service"`
}

// Item represents a purchased item
type Item struct {
	ListingHash    string         `json:"listingHash"`
	Quantity       string         `json:"bigQuantity"`
	Options        options        `json:"options"`
	Shipping       shippingOption `json:"shipping"`
	Memo           string         `json:"memo"`
	Coupons        []string       `json:"coupons"`
	PaymentAddress string         `json:"paymentAddress"`
}

// PurchaseData represents purchase request metadata
type PurchaseData struct {
	ShipTo               string  `json:"shipTo"`
	Address              string  `json:"address"`
	City                 string  `json:"city"`
	State                string  `json:"state"`
	PostalCode           string  `json:"postalCode"`
	CountryCode          string  `json:"countryCode"`
	AddressNotes         string  `json:"addressNotes"`
	Moderator            string  `json:"moderator"`
	Items                []Item  `json:"items"`
	AlternateContactInfo string  `json:"alternateContactInfo"`
	RefundAddress        *string `json:"refundAddress"` //optional, can be left out of json
	PaymentCoin          string  `json:"paymentCoin"`
}

// IndividualListingContainer is a wrapper for a single listing
type IndividualListingContainer struct {
	Listing `json:"listing"`
}

// Listing represents a trade offer which can be accepted by another
// party on the OpenBazaar network
type Listing struct {
	listingProto *pb.Listing

	proto.Message
}

func (l *Listing) Reset()         { *l = Listing{} }
func (l *Listing) String() string { return proto.CompactTextString(l) }
func (*Listing) ProtoMessage()    {}

// NewListingFromProtobuf - return Listing from pb.Listing
func NewListingFromProtobuf(l *pb.Listing) (*Listing, error) {
	clonedListing := proto.Clone(l).(*pb.Listing)

	if clonedListing.Metadata.Version == 0 {
		clonedListing.Metadata.Version = ListingVersion
	}
	if clonedListing.Metadata.EscrowTimeoutHours == 0 {
		clonedListing.Metadata.EscrowTimeoutHours = DefaultEscrowTimeout
	}
	return &Listing{
		listingProto: clonedListing,
	}, nil
}

// CreateListing will create a pb Listing
func CreateListing(r []byte, isTestnet bool, dstore *Datastore, repoPath string) (Listing, error) {
	ld := new(pb.Listing)
	err := jsonpb.UnmarshalString(string(r), ld)
	if err != nil {
		return Listing{}, err
	}
	slug := ld.Slug
	exists, err := listingExists(slug, repoPath, isTestnet)
	if err != nil {
		return Listing{}, err
	}
	if exists {
		return Listing{}, ErrListingAlreadyExists
	}
	if slug == "" {
		slug, err = GenerateSlug(ld.Item.Title, repoPath, isTestnet, dstore)
		if err != nil {
			return Listing{}, err
		}
		ld.Slug = slug
	}
	retListing, err := NewListingFromProtobuf(ld)

	return *retListing, err
}

// UpdateListing will update a pb Listing
func UpdateListing(r []byte, isTestnet bool, dstore *Datastore, repoPath string) (Listing, error) {
	ld := new(pb.Listing)
	err := jsonpb.UnmarshalString(string(r), ld)
	if err != nil {
		return Listing{}, err
	}
	skus := ld.Item.Skus
	for _, sku := range skus {
		if sku.BigSurcharge == "" {
			sku.BigSurcharge = "0"
		}
		if sku.BigQuantity == "" {
			sku.BigQuantity = "0"
		}
	}

	ld.Item.Skus = skus

	slug := ld.Slug
	exists, err := listingExists(slug, repoPath, isTestnet)
	if err != nil {
		return Listing{}, err
	}
	if !exists {
		return Listing{}, ErrListingDoesNotExist
	}
	retListing, err := NewListingFromProtobuf(ld)
	return *retListing, err
}

func listingExists(slug, repoPath string, isTestnet bool) (bool, error) {
	if slug == "" {
		return false, nil
	}
	fPath, err := GetPathForListingSlug(slug, repoPath, isTestnet)
	if err != nil {
		return false, err
	}
	_, ferr := os.Stat(fPath)
	if slug == "" {
		return false, nil
	}
	if os.IsNotExist(ferr) {
		return false, nil
	}
	if ferr != nil {
		return false, ferr
	}
	return true, nil
}

// GenerateSlug - slugify the title of the listing
func GenerateSlug(title, repoPath string, isTestnet bool, dStore *Datastore) (string, error) {
	title = strings.Replace(title, "/", "", -1)
	counter := 1
	slugBase := CreateSlugFor(title)
	slugToTry := slugBase
	for {
		_, err := GetListingFromSlug(slugToTry, repoPath, isTestnet, dStore)
		if os.IsNotExist(err) {
			return slugToTry, nil
		} else if err != nil {
			return "", err
		}
		slugToTry = slugBase + strconv.Itoa(counter)
		counter++
	}
}

// GetListingFromSlug - fetch listing for the specified slug
func GetListingFromSlug(slug, repoPath string, isTestnet bool, dStore *Datastore) (*pb.SignedListing, error) {
	repoPath, err := GetRepoPath(isTestnet, repoPath)
	if err != nil {
		return nil, err
	}
	// Read listing file
	listingPath := path.Join(repoPath, "root", "listings", slug+".json")
	file, err := ioutil.ReadFile(listingPath)
	if err != nil {
		return nil, err
	}

	// Unmarshal listing
	sl := new(pb.SignedListing)
	err = jsonpb.UnmarshalString(string(file), sl)
	if err != nil {
		return nil, err
	}

	// Get the listing inventory
	inventory, err := (*dStore).Inventory().Get(slug)
	if err != nil {
		return nil, err
	}

	// Build the inventory list
	for variant, count := range inventory {
		for i, s := range sl.Listing.Item.Skus {
			if variant == i {
				s.BigQuantity = count.String()
				break
			}
		}
	}

	for _, s := range sl.Listing.Item.Skus {
		if s.BigSurcharge == "" {
			s.BigSurcharge = "0"
		}
	}

	return sl, nil
}

func GetPathForListingSlug(slug, repoPath string, isTestnet bool) (string, error) {
	repoPath, err := GetRepoPath(isTestnet, repoPath)
	if err != nil {
		return "", err
	}
	return path.Join(repoPath, "root", "listings", slug+".json"), nil
}

func ToHtmlEntities(str string) string {
	var rx = regexp.MustCompile(util.EmojiPattern)
	return rx.ReplaceAllStringFunc(str, func(s string) string {
		r, _ := utf8.DecodeRuneInString(s)
		html := fmt.Sprintf(`&#x%X;`, r)
		return html
	})
}

// CreateSlugFor Create a slug from a multi-lang string
func CreateSlugFor(slugName string) string {
	l := SentenceMaxCharacters - SlugBuffer

	slugName = ToHtmlEntities(slugName)

	slug := slug.Make(slugName)
	if len(slug) < SentenceMaxCharacters-SlugBuffer {
		l = len(slug)
	}
	return slug[:l]
}

// ListingMetadata -
type ListingMetadata struct {
	Version uint `json:"version"`
}

// UnmarshalJSONListing - unmarshal listing
func UnmarshalJSONListing(data []byte) (*Listing, error) {
	l, err := UnmarshalJSONSignedListing(data)
	if err != nil {
		return nil, err
	}
	return l.GetListing(), nil
}

// ExtractVersion returns the version of the listing
func ExtractVersionFromSignedListing(data []byte) (uint, error) {
	type sl struct {
		Listing interface{} `json:"listing"`
	}
	var s sl
	err := json.Unmarshal(data, &s)

	if err != nil {
		return 0, err
	}

	lmap, ok := s.Listing.(map[string]interface{})
	if !ok {
		return 0, errors.New("malformed listing")
	}

	lampMeta0, ok := lmap["metadata"]
	if !ok {
		return 0, errors.New("malformed listing")
	}

	lampMeta, ok := lampMeta0.(map[string]interface{})
	if !ok {
		return 0, errors.New("malformed listing")
	}

	ver0, ok := lampMeta["version"]
	if !ok {
		return 0, errors.New("malformed listing")
	}

	ver, ok := ver0.(float64)
	if !ok {
		return 0, errors.New("malformed listing")
	}

	return uint(ver), nil
}

// ExtractIDFromListing returns pb.ID of the listing
func ExtractIDFromListing(data []byte) (*pb.ID, error) {
	var lmap map[string]*json.RawMessage
	vendorPlay := new(pb.ID)

	err := json.Unmarshal(data, &lmap)
	if err != nil {
		log.Error(err)
		return vendorPlay, err
	}

	err = json.Unmarshal(*lmap["vendorID"], &vendorPlay)
	if err != nil {
		log.Error(err)
		return vendorPlay, err
	}

	return vendorPlay, nil
}

// Normalize converts legacy schema listing data from other users on the network
// to fit the latest schema for consumption via the API for local use. NOTE: Legacy
// nodes do not understand the latest schema. As such, normalized listings must not
// be used as part of the RicardianContract and must be serialized and used as they
// were provided by the originating node.
func (l *Listing) Normalize() (*Listing, error) {
	if l == nil {
		return nil, errors.New("nil listing cannot be normalized")
	}

	if l.GetVersion() == ListingVersion {
		return l, nil
	}

	nl, err := NewListingFromProtobuf(l.listingProto)
	if err != nil {
		return nil, fmt.Errorf("creating listing clone: %s", err.Error())
	}

	nlp := nl.GetProtobuf()
	nlp.Metadata.Version = ListingVersion

	if p, err := l.GetPrice(); err != nil {
		return nil, fmt.Errorf("get price: %s", err.Error())
	} else {
		nlp.Item.BigPrice = p.Amount.String()
		nlp.Item.PriceCurrency = &pb.CurrencyDefinition{
			Code:         p.Currency.Code.String(),
			Divisibility: uint32(p.Currency.Divisibility),
		}
	}

	if ss, err := l.GetSkus(); err != nil {
		return nil, fmt.Errorf("get skus: %s", err.Error())
	} else {
		nlp.Item.Skus = ss
	}

	if sos, err := l.GetShippingOptions(); err != nil {
		return nil, fmt.Errorf("get shipping options: %s", err.Error())
	} else {
		nlp.ShippingOptions = sos
	}

	if cs, err := l.GetCoupons(); err != nil {
		return nil, fmt.Errorf("get coupons: %s", err.Error())
	} else {
		nlp.Coupons = cs.GetProtobuf()
	}

	return nl, nil
}

// GetProtobuf returns the current state of pb.Listing managed by Listing
func (l *Listing) GetProtobuf() *pb.Listing {
	return l.listingProto
}

// GetVersion returns the schema version of the Listing
func (l *Listing) GetVersion() uint32 {
	return l.listingProto.Metadata.Version
}

// GetCryptoDivisibility returns the listing crypto divisibility
func (l *Listing) GetCryptoDivisibility() uint32 {
	if l.GetContractType() != pb.Listing_Metadata_CRYPTOCURRENCY.String() {
		return 0
	}

	switch l.GetVersion() {
	case 5:
		return l.listingProto.Metadata.CryptoDivisibility
	default: // version < 4
		div := l.listingProto.Metadata.CryptoDivisibility
		if div != 0 {
			div = uint32(math.Log10(float64(div)))
		}
		return div
	}
}

// GetCryptoCurrencyCode returns the listing crypto currency code
func (l *Listing) GetCryptoCurrencyCode() string {
	if l.GetContractType() != pb.Listing_Metadata_CRYPTOCURRENCY.String() {
		return ""
	}

	return l.listingProto.Metadata.CryptoCurrencyCode
}

// GetTitle returns the listing item title
func (l *Listing) GetTitle() string {
	return l.listingProto.Item.Title
}

// GetSlug returns the listing slug
func (l *Listing) GetSlug() string {
	return l.listingProto.Slug
}

// GetAcceptedCurrencies returns the listing's list of accepted currency codes
func (l *Listing) GetAcceptedCurrencies() []string {
	return l.listingProto.Metadata.AcceptedCurrencies
}

// SetPrices
func (l *Listing) SetPrices(percentage float64) error {

	currentPrice, ok := new(big.Float).SetString(l.listingProto.Item.BigPrice)
	if !ok {
		return nil
	}

	multiple := percentage/100 + 1
	floatFactor := new(big.Float).SetFloat64(multiple)

	newPrice := new(big.Float).Mul(currentPrice, floatFactor)
	newPriceInt, _ := newPrice.Int(nil)

	// Check if new price is negative
	zeroInt, _ := new(big.Int).SetString("0", 10)
	if newPriceInt.Cmp(zeroInt) == -1 {
		l.listingProto.Item.BigPrice = "1"
	} else {
		l.listingProto.Item.BigPrice = newPriceInt.String()
	}

	return nil
}

// SetAcceptedCurrencies the listing's accepted currency codes. Assumes the node
// serving the listing has already validated the wallet supports the currencies.
func (l *Listing) SetAcceptedCurrencies(codes ...string) error {
	if len(codes) < 1 {
		return errors.New("no accepted currencies provided")
	}
	var accCurrencies = make([]string, 0)
	for _, c := range codes {
		def, err := AllCurrencies().Lookup(c)
		if err != nil {
			return fmt.Errorf("unknown accepted currency (%s)", c)
		}
		accCurrencies = append(accCurrencies, def.CurrencyCode().String())
	}
	l.listingProto.Metadata.AcceptedCurrencies = accCurrencies
	return nil
}

// GetContractType returns listing's contract type
func (l *Listing) GetContractType() string {
	return l.listingProto.Metadata.ContractType.String()
}

// GetFormat returns the listing's pricing format
func (l *Listing) GetFormat() string {
	return l.listingProto.Metadata.Format.String()
}

// GetPrice returns the listing price. For CRYPTOCURRENCY listings, this
// value would be zero in the denomination of the cryptocurrency being
// traded and the item value in all other cases. In the event that the
// shema version or contract type are unrecognizable, an error is returned.
func (l *Listing) GetPrice() (*CurrencyValue, error) {
	switch l.GetContractType() {
	case pb.Listing_Metadata_CRYPTOCURRENCY.String():
		return &CurrencyValue{
			Amount:   big.NewInt(0),
			Currency: NewUnknownCryptoDefinition(l.GetCryptoCurrencyCode(), 0),
		}, nil
	case pb.Listing_Metadata_DIGITAL_GOOD.String(), pb.Listing_Metadata_PHYSICAL_GOOD.String(), pb.Listing_Metadata_SERVICE.String():
		switch l.GetVersion() {
		case 5:
			return NewCurrencyValueFromProtobuf(l.listingProto.Item.BigPrice, l.listingProto.Item.PriceCurrency)
		case 4, 3, 2:
			priceCurrency, err := AllCurrencies().Lookup(l.listingProto.Metadata.PricingCurrency)
			if err != nil {
				return nil, fmt.Errorf("lookup metadata pricing currency: %s", err)
			}
			return NewCurrencyValueFromUint(l.listingProto.Item.Price, priceCurrency)
		default:
			return nil, fmt.Errorf("unknown schema version")
		}
	}
	return nil, fmt.Errorf("unknown contract type")
}

// GetModerators returns accepted moderators for the listing
func (l *Listing) GetModerators() []string {
	return l.listingProto.Moderators
}

// SetModerators updates the listing's accepted moderators
func (l *Listing) SetModerators(mods []string) error {
	l.listingProto.Moderators = mods
	// mutations should return an error, even if no error is possible today
	return nil
}

// GetTermsAndConditions return the terms for the listings purchase contract
func (l *Listing) GetTermsAndConditions() string {
	return l.listingProto.TermsAndConditions
}

// GetRefundPolicy return the refund policy for the listing
func (l *Listing) GetRefundPolicy() string {
	return l.listingProto.RefundPolicy
}

// GetVendorID returns the vendor peer ID
func (l *Listing) GetVendorID() *PeerInfo {
	return NewPeerInfoFromProtobuf(l.listingProto.VendorID)
}

// GetShortDescription returns the item description truncated down to the
// ShortDescriptionLength maximum
func (l *Listing) GetShortDescription() string {
	dl := len(l.GetDescription())
	if dl > ShortDescriptionLength {
		return l.GetDescription()[:ShortDescriptionLength]
	}
	return l.GetDescription()
}

// GetDescription returns item description
func (l *Listing) GetDescription() string {
	return l.listingProto.Item.Description
}

// GetProcessingTime returns the expected time for vendor to process listing fulfillment
func (l *Listing) GetProcessingTime() string {
	return l.listingProto.Item.ProcessingTime
}

// GetNSFW returns whether the listing is marked as inappropriate for general viewing
// or otherwise "Not Safe For Work"
func (l *Listing) GetNsfw() bool {
	return l.listingProto.Item.Nsfw
}

// GetTags returns a list of tags for the listing
func (l *Listing) GetTags() []string {
	return l.listingProto.Item.Tags
}

// GetCategories returns a list of categories for the listing
func (l *Listing) GetCategories() []string {
	return l.listingProto.Item.Categories
}

// GetGrams returns listing item weight in grams
func (l *Listing) GetWeightGrams() float32 {
	return l.listingProto.Item.Grams
}

// GetCondition returns listing item condition
func (l *Listing) GetCondition() string {
	return l.listingProto.Item.Condition
}

// ListingImage represents the underlying protobuf image
type ListingImage struct {
	listing    *Listing
	protoIndex int

	filename string
	original string
	large    string
	medium   string
	small    string
	tiny     string
}

// GetFilename returns the image filename
func (i *ListingImage) GetFilename() string { return i.filename }

// GetOriginal returns the image's original size
func (i *ListingImage) GetOriginal() string { return i.original }

// GetLarge returns the image's large size
func (i *ListingImage) GetLarge() string { return i.large }

// GetMedium returns the image's medium size
func (i *ListingImage) GetMedium() string { return i.medium }

// GetSmall returns the image's small size
func (i *ListingImage) GetSmall() string { return i.small }

// GetTiny returns the image's tiny size
func (i *ListingImage) GetTiny() string { return i.tiny }

// String satisfies Stringer and returns the image filename
func (i *ListingImage) String() string { return i.GetFilename() }

func (i *ListingImage) imageProtobuf() (*pb.Listing_Item_Image, error) {
	if i == nil ||
		i.listing == nil ||
		i.listing.listingProto == nil ||
		i.listing.listingProto.Item == nil ||
		i.listing.listingProto.Item.Images == nil {
		return nil, fmt.Errorf("listing item image incomplete")
	}
	pbImg := i.listing.listingProto.Item.Images[i.protoIndex]
	if i.filename != pbImg.GetFilename() ||
		i.original != pbImg.GetOriginal() ||
		i.large != pbImg.GetLarge() ||
		i.medium != pbImg.GetMedium() ||
		i.small != pbImg.GetSmall() ||
		i.tiny != pbImg.GetTiny() {
		return nil, fmt.Errorf("underlying protobuf has changed from expected state")
	}
	return pbImg, nil
}

// SetOriginal updates CID for the original image
func (i *ListingImage) SetOriginal(cid string) error {
	var pbi, err = i.imageProtobuf()
	if err != nil {
		return fmt.Errorf("set original image hash: %s", err.Error())
	}
	pbi.Original = cid
	i.original = cid
	return nil
}

// SetLarge updates CID for the large image
func (i *ListingImage) SetLarge(cid string) error {
	var pbi, err = i.imageProtobuf()
	if err != nil {
		return fmt.Errorf("set large image hash: %s", err.Error())
	}
	pbi.Large = cid
	i.large = cid
	return nil
}

// SetMedium updates CID for the medium image
func (i *ListingImage) SetMedium(cid string) error {
	var pbi, err = i.imageProtobuf()
	if err != nil {
		return fmt.Errorf("set medium image hash: %s", err.Error())
	}
	pbi.Medium = cid
	i.medium = cid
	return nil
}

// SetSmall updates CID for the small image
func (i *ListingImage) SetSmall(cid string) error {
	var pbi, err = i.imageProtobuf()
	if err != nil {
		return fmt.Errorf("set small image hash: %s", err.Error())
	}
	pbi.Small = cid
	i.small = cid
	return nil
}

// SetTiny updates CID for the tiny image
func (i *ListingImage) SetTiny(cid string) error {
	var pbi, err = i.imageProtobuf()
	if err != nil {
		return fmt.Errorf("set tiny image hash: %s", err.Error())
	}
	pbi.Tiny = cid
	i.tiny = cid
	return nil
}

// GetImages return set of listing item images
func (l *Listing) GetImages() []*ListingImage {
	if l == nil ||
		l.listingProto == nil ||
		l.listingProto.Item == nil ||
		l.listingProto.Item.Images == nil {
		return make([]*ListingImage, 0)
	}
	var (
		protoImgs = l.listingProto.Item.Images
		imgs      = make([]*ListingImage, len(protoImgs))
	)
	for i, img := range protoImgs {
		imgs[i] = &ListingImage{
			listing:    l,
			protoIndex: i,
			filename:   img.GetFilename(),
			original:   img.GetOriginal(),
			large:      img.GetLarge(),
			medium:     img.GetMedium(),
			small:      img.GetSmall(),
			tiny:       img.GetTiny(),
		}
	}
	return imgs
}

// GetSkus returns the listing SKUs
func (l *Listing) GetSkus() ([]*pb.Listing_Item_Sku, error) {
	var ss = make([]*pb.Listing_Item_Sku, len(l.listingProto.Item.Skus))
	for i, s := range l.listingProto.Item.Skus {
		ss[i] = proto.Clone(s).(*pb.Listing_Item_Sku)
	}
	switch l.GetVersion() {
	case 3, 4:
		for _, sku := range ss {
			sku.BigSurcharge = big.NewInt(sku.Surcharge).String()
			sku.BigQuantity = big.NewInt(sku.Quantity).String()
		}
	}
	return ss, nil
}

//GetLanguage return listing's language
func (l *Listing) GetLanguage() string {
	return l.listingProto.Metadata.Language
}

// GetEscrowTimeout return listing's escrow timeout in hours
func (l *Listing) GetEscrowTimeoutHours() uint32 {
	return l.listingProto.Metadata.EscrowTimeoutHours
}

// GetPriceModifier return listing's price modifier
func (l *Listing) GetPriceModifier() float32 {
	switch l.GetVersion() {
	case 5:
		return l.listingProto.Item.PriceModifier
	case 4, 3, 2:
		return l.listingProto.Metadata.PriceModifier
	}
	log.Errorf("missing price modifier for listing (%s)", l.GetSlug())
	return 0
}

// ListingTaxes is a set of taxes
type ListingTaxes []ListingTax

// ListingTax describes how a listing is taxed in each region
type ListingTax struct {
	taxType         string
	regions         []string
	rate            float32
	taxableShipping bool
}

// GetType returns the tax type
func (t ListingTax) GetType() string { return t.taxType }

// GetApplicableRegions returns the regions affected by the tax
func (t ListingTax) GetApplicableRegions() []string { return t.regions }

// GetRate returns the tax rate
func (t ListingTax) GetRate() float32 { return t.rate }

// GetTaxableShipping indicates whether the shipping is subject to the tax
func (t ListingTax) GetTaxableShipping() bool { return t.taxableShipping }

// GetTaxes returns listing tax information
func (l *Listing) GetTaxes() ListingTaxes {
	var ts = make([]ListingTax, len(l.listingProto.Taxes))
	for ti, tax := range l.listingProto.Taxes {
		var rs = make([]string, len(tax.TaxRegions))
		for ri, region := range tax.TaxRegions {
			rs[ri] = region.String()
		}

		ts[ti] = ListingTax{
			taxType:         tax.GetTaxType(),
			rate:            tax.GetPercentage(),
			taxableShipping: tax.GetTaxShipping(),
			regions:         rs,
		}
	}
	return ts
}

type couponGetter interface {
	Get(string) ([]Coupon, error)
}

// UpdateCouponsFromDatastore will get all coupons from the datastore and update
// the internal protobuf with the codes that match the coupon's hash, if any.
func (l *Listing) UpdateCouponsFromDatastore(cdb couponGetter) error {
	var coupons, err = l.GetCoupons()
	if err != nil {
		return fmt.Errorf("getting coupons: %s", err.Error())
	}
	dbCoupons, err := cdb.Get(l.GetSlug())
	if err != nil {
		return fmt.Errorf("loading datastore coupon: %s", err.Error())
	}
	for i, c := range coupons {
		for _, dbc := range dbCoupons {
			if c.redemptionHash == dbc.Hash {
				// make sure applying code does not shift already-matched hash
				expectedHash, err := ipfs.EncodeMultihash([]byte(dbc.Code))
				if err != nil {
					return fmt.Errorf("hashing persisted redemption code (%s): %s", dbc.Code, err.Error())
				}
				if c.redemptionHash != expectedHash.B58String() {
					return fmt.Errorf("update coupon code (%s) results in mismatched published hash", dbc.Code)
				}
				if err := coupons[i].SetRedemptionCode(dbc.Code); err != nil {
					return fmt.Errorf("setting redemption code: %s", err.Error())
				}
			}
		}
	}
	l.listingProto.Coupons = coupons.GetProtobuf()
	return nil
}

// GetCoupons returns listing coupons with discount amount normalized as a
// CurrencyValue
func (l *Listing) GetCoupons() (ListingCoupons, error) {
	var (
		protoCoupons   = l.listingProto.GetCoupons()
		cs             = make([]*ListingCoupon, len(protoCoupons))
		listingVersion = l.GetVersion()
		discPrice, err = l.GetPrice()
	)
	if err != nil {
		return nil, fmt.Errorf("get listing price for coupon: %s", err.Error())
	}

	for i, c := range protoCoupons {
		var (
			discAmt   string
			discValue *CurrencyValue
		)
		switch listingVersion {
		case 5:
			discAmt = c.GetBigPriceDiscount()
		default:
			//nolint:staticcheck
			discAmt = strconv.FormatUint(c.GetPriceDiscount(), 10)
		}
		if discAmt != "" {
			dv, err := NewCurrencyValue(discAmt, discPrice.Currency)
			if err != nil {
				return nil, fmt.Errorf("unable to create coupon discount value for amount (%s %s): %s", discAmt, discPrice.Currency, err.Error())
			}
			discValue = dv
		} else {
			discValue = nil
		}

		cs[i] = &ListingCoupon{
			listing:         l,
			title:           c.GetTitle(),
			redemptionCode:  c.GetDiscountCode(),
			redemptionHash:  c.GetHash(),
			discountPercent: c.GetPercentDiscount(),
			discountAmount:  discValue,
		}
	}
	return cs, nil
}

// ListingCoupons is a set of listing coupons
type ListingCoupons []*ListingCoupon

// GetProtobuf converts ListingCoupons into its protobuf representation
func (cs ListingCoupons) GetProtobuf() []*pb.Listing_Coupon {
	var cspb = make([]*pb.Listing_Coupon, len(cs))
	for i, c := range cs {
		cspb[i] = &pb.Listing_Coupon{
			Title: c.GetTitle(),
		}
		if c.GetPercentOff() > 0 {
			cspb[i].Discount = &pb.Listing_Coupon_PercentDiscount{
				PercentDiscount: c.GetPercentOff(),
			}
		} else if c.GetAmountOff() != nil {
			cspb[i].Discount = &pb.Listing_Coupon_BigPriceDiscount{
				BigPriceDiscount: c.GetAmountOff().Amount.String(),
			}
		}
		if hash, err := c.GetRedemptionHash(); err == nil {
			cspb[i].Code = &pb.Listing_Coupon_Hash{Hash: hash}
		}
		if code, err := c.GetRedemptionCode(); err == nil {
			cspb[i].Code = &pb.Listing_Coupon_DiscountCode{DiscountCode: code}
		}
	}
	return cspb
}

// ListingCoupon represents an coupon which can be applied to a listing for a discount
type ListingCoupon struct {
	listing *Listing

	title          string
	redemptionCode string
	redemptionHash string

	discountPercent float32
	discountAmount  *CurrencyValue
}

// GetListingSlug returns the slug for the coupon's listing
func (c *ListingCoupon) GetListingSlug() string { return c.listing.GetSlug() }

// GetTitle returns the coupon's title
func (c *ListingCoupon) GetTitle() string { return c.title }

// GetRedemptionCode returns the coupon redemption code
func (c *ListingCoupon) GetRedemptionCode() (string, error) {
	if c.redemptionCode != "" {
		return c.redemptionCode, nil
	}
	return "", errors.New("redemption code not set")
}

// GetRedemptionHash returns the hashed representation of the code
func (c *ListingCoupon) GetRedemptionHash() (string, error) {
	_, err := mh.FromB58String(c.redemptionHash)
	if err != nil {
		// if hash is invalid, let's try to produce a new one
		if c.redemptionCode == "" {
			return "", errors.New("hash invalid and code missing")
		}
		if err := c.SetRedemptionCode(c.redemptionCode); err != nil {
			return "", err
		}
	}
	return c.redemptionHash, nil
}

// GetPercentOff returns the percentage amount to reduce listing by
func (c *ListingCoupon) GetPercentOff() float32 { return c.discountPercent }

// GetAmountOff returns the value to reduce listing by
func (c *ListingCoupon) GetAmountOff() *CurrencyValue { return c.discountAmount }

// SetRedemptionCode sets the coupon's redemption code
func (c *ListingCoupon) SetRedemptionCode(code string) error {
	newHash, err := ipfs.EncodeMultihash([]byte(code))
	if err != nil {
		return fmt.Errorf("hashing redemption code: %s", err.Error())
	}
	// update proto first, otherwise ListingCoupon hash/code can't
	// be used to match against the correct proto to update
	if err := c.updateProtoHash(newHash.B58String()); err != nil {
		return err
	}
	c.redemptionCode = code
	c.redemptionHash = newHash.B58String()
	return nil
}

func (c *ListingCoupon) updateProtoHash(hash string) error {
	for _, pc := range c.listing.listingProto.Coupons {
		if pc.GetDiscountCode() == c.redemptionCode ||
			pc.GetHash() == c.redemptionHash {
			pc.Code = &pb.Listing_Coupon_Hash{Hash: hash}
			return nil
		}
	}
	return errors.New("unable to update missing coupon proto")
}

// GetShippingOptions returns all shipping options
func (l *Listing) GetShippingOptions() ([]*pb.Listing_ShippingOption, error) {
	var so = make([]*pb.Listing_ShippingOption, len(l.listingProto.ShippingOptions))
	for i, s := range l.listingProto.ShippingOptions {
		so[i] = proto.Clone(s).(*pb.Listing_ShippingOption)
	}
	switch l.GetVersion() {
	case 3, 4:
		for a, o := range so {
			for x, s := range o.Services {
				so[a].Services[x].BigPrice = big.NewInt(int64(s.Price)).String()
				so[a].Services[x].BigAdditionalItemPrice = big.NewInt(int64(s.AdditionalItemPrice)).String()
			}
		}
	case 5:
		for a, o := range so {
			for x, s := range o.Services {
				if s.BigPrice == "" {
					so[a].Services[x].BigPrice = "0"
				}
				if s.BigAdditionalItemPrice == "" {
					so[a].Services[x].BigAdditionalItemPrice = "0"
				}
			}
		}
	}
	return so, nil

}

// GetShippingRegions returns all region strings for the defined shipping
// services
func (l *Listing) GetShippingRegions() ([]string, []string) {
	var (
		shipsTo        = make(map[string]struct{})
		freeShippingTo = make(map[string]struct{})
	)
	for _, shipOption := range l.listingProto.ShippingOptions {
		for _, shipRegion := range shipOption.Regions {
			shipsTo[shipRegion.String()] = struct{}{}
			for _, shipService := range shipOption.Services {
				servicePrice, ok := new(big.Int).SetString(shipService.BigPrice, 10)
				if ok && servicePrice.Cmp(big.NewInt(0)) == 0 {
					freeShippingTo[shipRegion.String()] = struct{}{}
				}
			}
		}
	}

	var returnShipTo = make([]string, 0)
	for s := range shipsTo {
		returnShipTo = append(returnShipTo, s)
	}
	var returnFreeShipTo = make([]string, 0)
	for s := range freeShippingTo {
		returnFreeShipTo = append(returnFreeShipTo, s)
	}
	return returnShipTo, returnFreeShipTo
}

// MarshalProtobuf returns the byte serialization of the underlying protobuf
func (l *Listing) MarshalProtobuf() ([]byte, error) {
	return proto.Marshal(l.listingProto)
}

type listingSigner interface {
	TestNetworkEnabled() bool
	RegressionNetworkEnabled() bool
	GetNodeID() (*pb.ID, error)
	Sign([]byte) ([]byte, error)
}

// MarshalJSON returns the json serialization of the underlying protobuf
func (l *Listing) MarshalJSON() ([]byte, error) {
	m := jsonpb.Marshaler{
		EnumsAsInts:  false,
		EmitDefaults: false,
		Indent:       "    ",
		OrigName:     false,
	}
	lb, err := m.MarshalToString(l.listingProto)
	if err != nil {
		return nil, err
	}
	return []byte(lb), nil
}

// Sign verifies the Listing and returns a SignedListing
func (l *Listing) Sign(n listingSigner) (*SignedListing, error) {
	var (
		timeout   = l.GetEscrowTimeoutHours()
		isTestnet = n.TestNetworkEnabled() || n.RegressionNetworkEnabled()
	)

	// Temporary hack to work around test env shortcomings
	if isTestnet {
		timeout = 1
	}
	l.listingProto.Metadata.EscrowTimeoutHours = timeout

	// Set inventory to the default as it's not part of the contract
	for _, s := range l.listingProto.Item.Skus {
		s.Quantity = 0
		s.BigQuantity = "0"
	}

	// Check the listing data is correct for continuing
	if err := l.ValidateListing(isTestnet); err != nil {
		return nil, err
	}

	// Sanitize a few critical fields
	sanitizer := bluemonday.UGCPolicy()
	for _, opt := range l.listingProto.Item.Options {
		opt.Name = sanitizer.Sanitize(opt.Name)
		for _, v := range opt.Variants {
			v.Name = sanitizer.Sanitize(v.Name)
		}
	}
	for _, so := range l.listingProto.ShippingOptions {
		so.Name = sanitizer.Sanitize(so.Name)
		for _, serv := range so.Services {
			serv.Name = sanitizer.Sanitize(serv.Name)
		}
	}

	// Add the vendor ID to the listing
	id, err := n.GetNodeID()
	if err != nil {
		return nil, fmt.Errorf("vendor id: %s", err.Error())
	}
	l.listingProto.VendorID = id

	// Sign listing
	serializedListing, err := l.MarshalProtobuf()
	if err != nil {
		return nil, fmt.Errorf("serializing listing: %s", err.Error())
	}
	listingSig, err := n.Sign(serializedListing)
	if err != nil {
		return nil, fmt.Errorf("signing listing: %s", err.Error())
	}

	sl := new(pb.SignedListing)
	sl.Listing = l.listingProto
	sl.Signature = listingSig

	return &SignedListing{
		signedListingProto: sl,
	}, nil
}

// ValidateSkus ensures valid SKU state
func (l *Listing) ValidateSkus() error {
	if l.listingProto.Metadata.ContractType == pb.Listing_Metadata_CRYPTOCURRENCY {
		return validateCryptocurrencyQuantity(l.listingProto)
	}
	return nil
}

func validateCryptocurrencyQuantity(listing *pb.Listing) error {
	var checkFn func(*pb.Listing_Item_Sku) error
	switch listing.Metadata.Version {
	case 5:
		checkFn = func(s *pb.Listing_Item_Sku) error {
			if s == nil {
				return fmt.Errorf("cannot validate nil sku")
			}
			if s.BigQuantity == "" {
				return fmt.Errorf("sku quantity empty")
			}
			if ba, ok := new(big.Int).SetString(s.BigQuantity, 10); ok && ba.Cmp(big.NewInt(0)) < 0 {
				return fmt.Errorf("sku quantity cannot be negative")
			}
			return nil
		}
	default:
		checkFn = func(s *pb.Listing_Item_Sku) error {
			if s == nil {
				return fmt.Errorf("cannot validate nil sku")
			}
			if s.Quantity <= 0 {
				return fmt.Errorf("sku quantity zero or less")
			}
			return nil
		}
	}
	for _, sku := range listing.Item.Skus {
		if err := checkFn(sku); err != nil {
			return ErrCryptocurrencySkuQuantityInvalid
		}
	}
	return nil
}

// GetInventory - returns a map of skus and quantityies
func (l *Listing) GetInventory() (map[int]*big.Int, error) {
	inventory := make(map[int]*big.Int)
	for i, s := range l.listingProto.Item.Skus {
		var amtStr string
		switch l.GetVersion() {
		case 5:
			if s.BigQuantity == "" {
				continue
			}
			amtStr = s.BigQuantity
		default:
			amtStr = strconv.Itoa(int(s.Quantity))
		}
		amt, ok := new(big.Int).SetString(amtStr, 10)
		if !ok {
			return nil, errors.New("error parsing inventory")
		}
		inventory[i] = amt
	}
	return inventory, nil
}

// ValidateListing ensures all listing state is valid
func (l *Listing) ValidateListing(testnet bool) (err error) {
	defer func() {
		if r := recover(); r != nil {
			switch x := r.(type) {
			case string:
				err = errors.New(x)
			case error:
				err = x
			default:
				err = errors.New("unknown panic")
			}
		}
	}()

	// Slug
	if l.listingProto.Slug == "" {
		return errors.New("slug must not be empty")
	}
	if len(l.listingProto.Slug) > SentenceMaxCharacters {
		return fmt.Errorf("slug is longer than the max of %d", SentenceMaxCharacters)
	}
	if strings.Contains(l.listingProto.Slug, " ") {
		return errors.New("slugs cannot contain spaces")
	}
	if strings.Contains(l.listingProto.Slug, "/") {
		return errors.New("slugs cannot contain file separators")
	}

	// Metadata
	if l.listingProto.Metadata == nil {
		return errors.New("missing required field: Metadata")
	}
	if l.listingProto.Metadata.ContractType > pb.Listing_Metadata_CRYPTOCURRENCY {
		return errors.New("invalid contract type")
	}
	if l.listingProto.Metadata.Format > pb.Listing_Metadata_MARKET_PRICE {
		return errors.New("invalid listing format")
	}
	if l.listingProto.Metadata.Expiry == nil {
		return errors.New("missing required field: Expiry")
	}
	if time.Unix(l.listingProto.Metadata.Expiry.Seconds, 0).Before(time.Now()) {
		return errors.New("listing expiration must be in the future")
	}
	if len(l.listingProto.Metadata.Language) > WordMaxCharacters {
		return fmt.Errorf("language is longer than the max of %d characters", WordMaxCharacters)
	}

	if !testnet && l.listingProto.Metadata.EscrowTimeoutHours != DefaultEscrowTimeout {
		return fmt.Errorf("escrow timeout must be %d hours", DefaultEscrowTimeout)
	}
	if len(l.listingProto.Metadata.AcceptedCurrencies) == 0 {
		return errors.New("at least one accepted currency must be provided")
	}
	if len(l.listingProto.Metadata.AcceptedCurrencies) > MaxListItems {
		return fmt.Errorf("acceptedCurrencies is longer than the max of %d currencies", MaxListItems)
	}
	for _, c := range l.listingProto.Metadata.AcceptedCurrencies {
		if len(c) > WordMaxCharacters {
			return fmt.Errorf("accepted currency is longer than the max of %d characters", WordMaxCharacters)
		}
	}

	// Item
	if l.listingProto.Item == nil {
		return errors.New("no item in listing")
	}
	if l.listingProto.Item.Title == "" {
		return errors.New("listing must have a title")
	}
	if l.listingProto.Metadata.ContractType != pb.Listing_Metadata_CRYPTOCURRENCY && l.listingProto.Item.BigPrice == "0" {
		return errors.New("zero price listings are not allowed")
	}
	if len(l.listingProto.Item.Title) > TitleMaxCharacters {
		return fmt.Errorf("title is longer than the max of %d characters", TitleMaxCharacters)
	}
	if len(l.listingProto.Item.Description) > DescriptionMaxCharacters {
		return fmt.Errorf("description is longer than the max of %d characters", DescriptionMaxCharacters)
	}
	if len(l.listingProto.Item.ProcessingTime) > SentenceMaxCharacters {
		return fmt.Errorf("processing time length must be less than the max of %d", SentenceMaxCharacters)
	}
	if len(l.listingProto.Item.Tags) > MaxTags {
		return fmt.Errorf("number of tags exceeds the max of %d", MaxTags)
	}
	for _, tag := range l.listingProto.Item.Tags {
		if tag == "" {
			return errors.New("tags must not be empty")
		}
		if len(tag) > WordMaxCharacters {
			return fmt.Errorf("tags must be less than max of %d", WordMaxCharacters)
		}
	}
	if len(l.listingProto.Item.Images) == 0 {
		return errors.New("listing must contain at least one image")
	}
	if len(l.listingProto.Item.Images) > MaxListItems {
		return fmt.Errorf("number of listing images is greater than the max of %d", MaxListItems)
	}
	for _, img := range l.listingProto.Item.Images {
		_, err := cid.Decode(img.Tiny)
		if err != nil {
			return errors.New("tiny image hashes must be properly formatted CID")
		}
		_, err = cid.Decode(img.Small)
		if err != nil {
			return errors.New("small image hashes must be properly formatted CID")
		}
		_, err = cid.Decode(img.Medium)
		if err != nil {
			return errors.New("medium image hashes must be properly formatted CID")
		}
		_, err = cid.Decode(img.Large)
		if err != nil {
			return errors.New("large image hashes must be properly formatted CID")
		}
		_, err = cid.Decode(img.Original)
		if err != nil {
			return errors.New("original image hashes must be properly formatted CID")
		}
		if img.Filename == "" {
			return errors.New("image file names must not be nil")
		}
		if len(img.Filename) > FilenameMaxCharacters {
			return fmt.Errorf("image filename length must be less than the max of %d", FilenameMaxCharacters)
		}
	}
	if len(l.listingProto.Item.Categories) > MaxCategories {
		return fmt.Errorf("number of categories must be less than max of %d", MaxCategories)
	}
	for _, category := range l.listingProto.Item.Categories {
		if category == "" {
			return errors.New("categories must not be nil")
		}
		if len(category) > WordMaxCharacters {
			return fmt.Errorf("category length must be less than the max of %d", WordMaxCharacters)
		}
	}

	maxCombos := 1
	variantSizeMap := make(map[int]int)
	optionMap := make(map[string]struct{})
	for i, option := range l.listingProto.Item.Options {
		if _, ok := optionMap[option.Name]; ok {
			return errors.New("option names must be unique")
		}
		if option.Name == "" {
			return errors.New("options titles must not be empty")
		}
		if len(option.Variants) < 2 {
			return errors.New("options must have more than one variants")
		}
		if len(option.Name) > WordMaxCharacters {
			return fmt.Errorf("option title length must be less than the max of %d", WordMaxCharacters)
		}
		if len(option.Description) > SentenceMaxCharacters {
			return fmt.Errorf("option description length must be less than the max of %d", SentenceMaxCharacters)
		}
		if len(option.Variants) > MaxListItems {
			return fmt.Errorf("number of variants is greater than the max of %d", MaxListItems)
		}
		varMap := make(map[string]struct{})
		for _, variant := range option.Variants {
			if _, ok := varMap[variant.Name]; ok {
				return errors.New("variant names must be unique")
			}
			if len(variant.Name) > WordMaxCharacters {
				return fmt.Errorf("variant name length must be less than the max of %d", WordMaxCharacters)
			}
			if variant.Image != nil && (variant.Image.Filename != "" ||
				variant.Image.Large != "" || variant.Image.Medium != "" || variant.Image.Small != "" ||
				variant.Image.Tiny != "" || variant.Image.Original != "") {
				_, err := cid.Decode(variant.Image.Tiny)
				if err != nil {
					return errors.New("tiny image hashes must be properly formatted CID")
				}
				_, err = cid.Decode(variant.Image.Small)
				if err != nil {
					return errors.New("small image hashes must be properly formatted CID")
				}
				_, err = cid.Decode(variant.Image.Medium)
				if err != nil {
					return errors.New("medium image hashes must be properly formatted CID")
				}
				_, err = cid.Decode(variant.Image.Large)
				if err != nil {
					return errors.New("large image hashes must be properly formatted CID")
				}
				_, err = cid.Decode(variant.Image.Original)
				if err != nil {
					return errors.New("original image hashes must be properly formatted CID")
				}
				if variant.Image.Filename == "" {
					return errors.New("image file names must not be nil")
				}
				if len(variant.Image.Filename) > FilenameMaxCharacters {
					return fmt.Errorf("image filename length must be less than the max of %d", FilenameMaxCharacters)
				}
			}
			varMap[variant.Name] = struct{}{}
		}
		variantSizeMap[i] = len(option.Variants)
		maxCombos *= len(option.Variants)
		optionMap[option.Name] = struct{}{}
	}

	if len(l.listingProto.Item.Skus) > maxCombos {
		return errors.New("more skus than variant combinations")
	}
	comboMap := make(map[string]bool)
	for _, sku := range l.listingProto.Item.Skus {
		if maxCombos > 1 && len(sku.VariantCombo) == 0 {
			return errors.New("skus must specify a variant combo when options are used")
		}
		if len(sku.ProductID) > WordMaxCharacters {
			return fmt.Errorf("product ID length must be less than the max of %d", WordMaxCharacters)
		}
		formatted, err := json.Marshal(sku.VariantCombo)
		if err != nil {
			return err
		}
		_, ok := comboMap[string(formatted)]
		if !ok {
			comboMap[string(formatted)] = true
		} else {
			return errors.New("duplicate sku")
		}
		if len(sku.VariantCombo) != len(l.listingProto.Item.Options) {
			return errors.New("incorrect number of variants in sku combination")
		}
		for i, combo := range sku.VariantCombo {
			if int(combo) > variantSizeMap[i] {
				return errors.New("invalid sku variant combination")
			}
		}

	}

	// Taxes
	if len(l.listingProto.Taxes) > MaxListItems {
		return fmt.Errorf("number of taxes is greater than the max of %d", MaxListItems)
	}
	for _, tax := range l.listingProto.Taxes {
		if tax.TaxType == "" {
			return errors.New("tax type must be specified")
		}
		if len(tax.TaxType) > WordMaxCharacters {
			return fmt.Errorf("tax type length must be less than the max of %d", WordMaxCharacters)
		}
		if len(tax.TaxRegions) == 0 {
			return errors.New("tax must specify at least one region")
		}
		if len(tax.TaxRegions) > MaxCountryCodes {
			return fmt.Errorf("number of tax regions is greater than the max of %d", MaxCountryCodes)
		}
		if tax.Percentage == 0 || tax.Percentage > 100 {
			return errors.New("tax percentage must be between 0 and 100")
		}
	}

	// Coupons
	if len(l.listingProto.Coupons) > MaxListItems {
		return fmt.Errorf("number of coupons is greater than the max of %d", MaxListItems)
	}
	for _, coupon := range l.listingProto.Coupons {
		if len(coupon.Title) > CouponTitleMaxCharacters {
			return fmt.Errorf("coupon title length must be less than the max of %d", SentenceMaxCharacters)
		}
		if len(coupon.GetDiscountCode()) > CodeMaxCharacters {
			return fmt.Errorf("coupon code length must be less than the max of %d", CodeMaxCharacters)
		}
		if coupon.GetPercentDiscount() > 100 {
			return errors.New("percent discount cannot be over 100 percent")
		}
		price, err := l.GetPrice()
		if err != nil {
			return err
		}
		if coupon.GetBigPriceDiscount() != "" {
			discount0, ok := new(big.Int).SetString(coupon.GetBigPriceDiscount(), 10)
			if !ok {
				return errors.New("coupon discount was invalid")
			}
			if price.Amount.Cmp(discount0) < 0 {
				return errors.New("price discount cannot be greater than the item price")
			}
		}
		if coupon.GetPercentDiscount() == 0 && coupon.GetBigPriceDiscount() == "" {
			return errors.New("coupons must have at least one positive discount value")
		}
		if coupon.GetPercentDiscount() != 0 && coupon.GetBigPriceDiscount() != "" {
			return errors.New("coupons must have either a percent discount or a fixed amount discount, but not both")
		}
	}

	// Moderators
	if len(l.listingProto.Moderators) > MaxListItems {
		return fmt.Errorf("number of moderators is greater than the max of %d", MaxListItems)
	}
	for _, moderator := range l.listingProto.Moderators {
		_, err := mh.FromB58String(moderator)
		if err != nil {
			return errors.New("moderator IDs must be multihashes")
		}
	}

	// TermsAndConditions
	if len(l.listingProto.TermsAndConditions) > PolicyMaxCharacters {
		return fmt.Errorf("terms and conditions length must be less than the max of %d", PolicyMaxCharacters)
	}

	// RefundPolicy
	if len(l.listingProto.RefundPolicy) > PolicyMaxCharacters {
		return fmt.Errorf("refund policy length must be less than the max of %d", PolicyMaxCharacters)
	}

	// Type-specific validations
	if l.listingProto.Metadata.ContractType == pb.Listing_Metadata_PHYSICAL_GOOD {
		err := l.validatePhysicalListing()
		if err != nil {
			return err
		}
	} else if l.listingProto.Metadata.ContractType == pb.Listing_Metadata_CRYPTOCURRENCY {
		err := l.ValidateCryptoListing()
		if err != nil {
			return err
		}
	}

	// Non-crypto validations
	if l.listingProto.Metadata.ContractType != pb.Listing_Metadata_CRYPTOCURRENCY {
		price, err := l.GetPrice()
		if err != nil {
			return err
		}
		if price.Currency.Code == "" {
			return errors.New("pricing currency is missing")
		}
		if priceCurrency, err := AllCurrencies().Lookup(price.Currency.Code.String()); err != nil {
			return errors.New("invalid pricing currency")
		} else {
			if price.Currency.Divisibility > priceCurrency.Divisibility {
				return errors.New("pricing currency divisibility is too large")
			}
		}
	}

	// Format-specific validations
	if l.listingProto.Metadata.Format == pb.Listing_Metadata_MARKET_PRICE {
		err := validateMarketPriceListing(l.listingProto)
		if err != nil {
			return err
		}
	}

	return nil
}

func (l *Listing) validatePhysicalListing() error {
	if len(l.listingProto.Item.Condition) > SentenceMaxCharacters {
		return fmt.Errorf("'Condition' length must be less than the max of %d", SentenceMaxCharacters)
	}
	if len(l.listingProto.Item.Options) > MaxListItems {
		return fmt.Errorf("number of options is greater than the max of %d", MaxListItems)
	}

	// ShippingOptions
	if len(l.listingProto.ShippingOptions) == 0 {
		return errors.New("must be at least one shipping option for a physical good")
	}
	if len(l.listingProto.ShippingOptions) > MaxListItems {
		return fmt.Errorf("number of shipping options is greater than the max of %d", MaxListItems)
	}
	var shippingTitles []string
	shippingOptions, err := l.GetShippingOptions()
	if err != nil {
		return err
	}
	for _, shippingOption := range shippingOptions {
		if shippingOption.Name == "" {
			return errors.New("shipping option title name must not be empty")
		}
		if len(shippingOption.Name) > WordMaxCharacters {
			return fmt.Errorf("shipping option service length must be less than the max of %d", WordMaxCharacters)
		}
		for _, t := range shippingTitles {
			if t == shippingOption.Name {
				return errors.New("shipping option titles must be unique")
			}
		}
		shippingTitles = append(shippingTitles, shippingOption.Name)
		if shippingOption.Type > pb.Listing_ShippingOption_FIXED_PRICE {
			return errors.New("unknown shipping option type")
		}
		if len(shippingOption.Regions) == 0 {
			return errors.New("shipping options must specify at least one region")
		}
		if err := ValidateShippingRegion(shippingOption); err != nil {
			return fmt.Errorf("invalid shipping option (%s): %s", shippingOption.String(), err.Error())
		}
		if len(shippingOption.Regions) > MaxCountryCodes {
			return fmt.Errorf("number of shipping regions is greater than the max of %d", MaxCountryCodes)
		}
		if len(shippingOption.Services) == 0 && shippingOption.Type != pb.Listing_ShippingOption_LOCAL_PICKUP {
			return errors.New("at least one service must be specified for a shipping option when not local pickup")
		}
		if len(shippingOption.Services) > MaxListItems {
			return fmt.Errorf("number of shipping services is greater than the max of %d", MaxListItems)
		}
		var serviceTitles []string
		for _, option := range shippingOption.Services {
			if option.Name == "" {
				return errors.New("shipping option service name must not be empty")
			}
			if len(option.Name) > WordMaxCharacters {
				return fmt.Errorf("shipping option service length must be less than the max of %d", WordMaxCharacters)
			}
			for _, t := range serviceTitles {
				if t == option.Name {
					return errors.New("shipping option services names must be unique")
				}
			}
			serviceTitles = append(serviceTitles, option.Name)

			if option.EstimatedDelivery == "" {
				return errors.New("shipping option estimated delivery must not be empty")
			}
			if len(option.EstimatedDelivery) > SentenceMaxCharacters {
				return fmt.Errorf("shipping option estimated delivery length must be less than the max of %d", SentenceMaxCharacters)
			}
			if _, ok := new(big.Int).SetString(option.BigPrice, 10); !ok {
				return errors.New("invalid shipping service price amount")
			}
		}
	}

	return nil
}

func (l *Listing) ValidateCryptoListing() error {
	if len(l.listingProto.Metadata.AcceptedCurrencies) != 1 {
		return errors.New("cryptocurrency listing must only have one accepted currency")
	}

	if len(l.listingProto.Coupons) > 0 {
		return ErrCryptocurrencyListingIllegalField("coupons")
	}
	if len(l.listingProto.Item.Options) > 0 {
		return ErrCryptocurrencyListingIllegalField("item.options")
	}
	if len(l.listingProto.ShippingOptions) > 0 {
		return ErrCryptocurrencyListingIllegalField("shippingOptions")
	}
	if len(l.listingProto.Item.Condition) > 0 {
		return ErrCryptocurrencyListingIllegalField("item.condition")
	}
	if l.listingProto.Item.PriceCurrency != nil &&
		len(l.listingProto.Item.PriceCurrency.Code) > 0 {
		return ErrCryptocurrencyListingIllegalField("item.pricingCurrency")
	}
	if len(l.listingProto.Metadata.CryptoCurrencyCode) == 0 {
		return ErrListingCryptoCurrencyCodeInvalid
	}

	cryptoDivisibility := l.GetCryptoDivisibility()
	if cryptoDivisibility == 0 {
		return ErrListingCryptoDivisibilityInvalid
	}
	def := NewUnknownCryptoDefinition(l.listingProto.Metadata.CryptoCurrencyCode, uint(cryptoDivisibility))
	if err := def.Valid(); err != nil {
		return fmt.Errorf("cryptocurrency metadata invalid: %s", err)
	}
	return nil
}

// SetCryptocurrencyListingDefaults ensures appropriate defaults are set for Crypto listings
func (l *Listing) SetCryptocurrencyListingDefaults() error {
	l.listingProto.Coupons = []*pb.Listing_Coupon{}
	l.listingProto.Item.Options = []*pb.Listing_Item_Option{}
	l.listingProto.ShippingOptions = []*pb.Listing_ShippingOption{}
	l.listingProto.Metadata.Format = pb.Listing_Metadata_MARKET_PRICE
	return nil
}

func validateMarketPriceListing(listing *pb.Listing) error {
	var (
		priceModifier   float32
		roundHundredths = func(f float32) float32 { return float32(int(f*100.0)) / 100.0 }
		n, ok           = new(big.Int).SetString(listing.Item.BigPrice, 10)
	)

	if ok && n.Cmp(big.NewInt(0)) != 0 {
		return ErrMarketPriceListingIllegalField("item.bigPrice")
	}

	if listing.Metadata.PriceModifier != 0 {
		priceModifier = roundHundredths(listing.Metadata.PriceModifier)
		listing.Metadata.PriceModifier = priceModifier
	} else if listing.Item.PriceModifier != 0 {
		priceModifier = roundHundredths(listing.Item.PriceModifier)
		listing.Item.PriceModifier = priceModifier
	}

	if priceModifier < PriceModifierMin ||
		priceModifier > PriceModifierMax {
		return ErrPriceModifierOutOfRange{
			Min: PriceModifierMin,
			Max: PriceModifierMax,
		}
	}

	return nil
}

// ValidateShippingRegion ensures shipping options are valid
func ValidateShippingRegion(shippingOption *pb.Listing_ShippingOption) error {
	for _, region := range shippingOption.Regions {
		if int32(region) == 0 {
			return ErrShippingRegionMustBeSet
		}
		_, ok := proto.EnumValueMap("CountryCode")[region.String()]
		if !ok {
			return ErrShippingRegionUndefined
		}
		if ok {
			if int32(region) > 500 {
				return ErrShippingRegionMustNotBeContinent
			}
		}
	}
	return nil
}

// ValidatePurchaseItemOptions ensures item options are valid
func (l *Listing) ValidatePurchaseItemOptions(itemOptions []option) error {
	if l.GetContractType() == pb.Listing_Metadata_CRYPTOCURRENCY.String() &&
		len(itemOptions) > 0 {
		return fmt.Errorf("options on a %s listing were provided, but are not supported", pb.Listing_Metadata_CRYPTOCURRENCY.String())
	}

	var (
		optSet     = make(map[string]struct{})
		checkedOpt = make(map[string]struct{})
	)
	// create an option set
	for _, lo := range l.listingProto.Item.Options {
		optSet[lo.Name] = struct{}{}
	}
	// walk through purchase options and verify
	for _, po := range itemOptions {
		// that they are available on the listing
		if _, ok := optSet[po.Name]; !ok {
			return fmt.Errorf("unknown item option (%s)", po.Name)
		}
		// that they haven't already been applied
		if _, ok := checkedOpt[po.Name]; ok {
			return fmt.Errorf("item option (%s) applied more than once", po.Name)
		}
		checkedOpt[po.Name] = struct{}{}
	}
	return nil
}

// ValidateListingOptions is a helper to ensure item options are valid
func ValidateListingOptions(listingItemOptions []*pb.Listing_Item_Option, itemOptions []option) ([]*pb.Order_Item_Option, error) {
	var validatedListingOptions []*pb.Order_Item_Option
	listingOptions := make(map[string]*pb.Listing_Item_Option)
	for _, opt := range listingItemOptions {
		listingOptions[strings.ToLower(opt.Name)] = opt
	}
	for _, uopt := range itemOptions {
		_, ok := listingOptions[strings.ToLower(uopt.Name)]
		if !ok {
			return nil, errors.New("selected variant not in listing")
		}
		delete(listingOptions, strings.ToLower(uopt.Name))
	}
	if len(listingOptions) > 0 {
		return nil, errors.New("Not all options were selected")
	}

	for _, option := range itemOptions {
		o := &pb.Order_Item_Option{
			Name:  option.Name,
			Value: option.Value,
		}
		validatedListingOptions = append(validatedListingOptions, o)
	}
	return validatedListingOptions, nil
}


text:
module Packet
  class Client
    module Plans
      def list_plans(*args)
        get('plans', *args).body['plans'].map { |p| Packet::Plan.new(p, self) }
      end
    end
  end
end

redpajama_set_name: RedPajamaGithub


text:
package com.fs.app.recsys.etl.utils;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class DBHelper {

	public String url = "jdbc:mysql://10.0.30.59/ptdb";
    public String name = "com.mysql.jdbc.Driver";
    public String user = "platform_app";
    public String password = "yV2x60A3";

    public Connection conn = null;
    public PreparedStatement pst = null;

    public void initConf(String _url,String _name,String _user,String _password){
    	this.url=_url;
    	this.name=_name;
    	this.user=_user;
    	this.password=_password;
    }
    public List<Map<String,String>> execQuery(String sql) {
    	List<Map<String,String>> result=new ArrayList<Map<String,String>>();
    	List<String> metas=new ArrayList<String>();
        try {
            Class.forName(name);//指定连接类型
            conn = DriverManager.getConnection(url, user, password);//获取连接
            pst = conn.prepareStatement(sql);//准备执行语句
            ResultSet ret=pst.executeQuery();
            ResultSetMetaData retMetaData= ret.getMetaData();
            for (int i = 1; i < retMetaData.getColumnCount(); i++) {
            	String _name=retMetaData.getColumnLabel(i);
            	metas.add(_name);
			}
            while (ret.next()) {
            	Map<String,String> map=new HashMap<String,String>();
            	for (String str : metas) {
            		map.put(str, ret.getString(str));
				}
            	result.add(map);
            }//显示数据
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
        	try {
				this.conn.close();
				this.pst.close();
			} catch (SQLException e) {
				e.printStackTrace();
			}
        }
        return result;
    }
}


text:
/* eslint no-console: 0 */
'use strict';
var _ = require('underscore');
var path = require('path');
var juttle_test_utils = require('juttle/test').utils;
var check_juttle = juttle_test_utils.check_juttle;
var expect = require('chai').expect;
var read_config = require('juttle/lib/config/read-config');

describe('cloudwatch adapter', function() {

    before(function() {

        // Try to read from the config file first. If not present,
        // look in the environment variable JUTTLE_AWS_CONFIG. In
        // TravisCI, the config is provided via the environment to
        // avoid putting sensitive information like ids/auth tokens in
        // source files.

        var config = read_config();
        var cloudwatch_config;

        if (_.has(config, 'adapters') &&
            _.has(config.adapters, 'cloudwatch')) {
            cloudwatch_config = config.adapters.cloudwatch;
        } else {

            if (! _.has(process.env, 'JUTTLE_CLOUDWATCH_CONFIG') ||
                process.env.JUTTLE_CLOUDWATCH_CONFIG === '') {
                throw new Error('To run this test, you must provide the adapter config via the environment as JUTTLE_CLOUDWATCH_CONFIG.');
            }
            cloudwatch_config = JSON.parse(process.env.JUTTLE_CLOUDWATCH_CONFIG);
        }

        cloudwatch_config.path = path.resolve(__dirname, '..');

        juttle_test_utils.configureAdapter({
            cloudwatch: cloudwatch_config
        });
    });

    describe('properly returns errors/warnings for arguments like', function() {

        it('an -every less than 5 minutes', function() {
            return check_juttle({
                program: 'read cloudwatch -every :1m: product="EC2" | view table'
            })
            .then(function(result) {
                expect(result.errors).to.have.length(0);
                expect(result.warnings).to.have.length(1);
                expect(result.warnings[0]).to.contain('-every of < 5 minutes may miss CloudWatch metrics');
            });
        });

        it('an -every less than 1 minute', function() {
            return check_juttle({
                program: 'read cloudwatch -every :1s: product="EC2" | view table'
            }).then(() => {
                throw new Error('Program ran when it should have returned an error');
            })
            .catch(function(err) {
                expect(err.code).to.equal('ADAPTER-UNSUPPORTED-TIME-OPTION');
            });
        });

        // XXX/mstemm ideally we'd like to have tests that test the
        // config-level overrides for -every warnings and errors work,
        // but there isn't any way to reconfigure or unload an adapter
        // right now.
    });

    describe('can read metrics', function() {

        function validate_point(point, statistics) {
            var expected_dimensions = {
                AutoScaling: 'AutoScalingGroupName',
                CloudFront: 'DistributionId',
                EBS: 'VolumeId',
                EC2: 'InstanceId',
                ElastiCache: 'CacheClusterId',
                ELB: 'LoadBalancerName',
                Lambda: 'FunctionName',
                RDS: 'DBInstanceIdentifier'
            };

            var expected_metrics = {
                'AWS/AutoScaling': {
                    GroupMinSize: {units: 'Count'},
                    GroupMaxSize: {units: 'Count'},
                    GroupDesiredCapacity: {units: 'Count'},
                    GroupInServiceInstances: {units: 'Count'},
                    GroupPendingInstances: {units: 'Count'},
                    GroupStandbyInstances: {units: 'Count'},
                    GroupTerminatingInstances: {units: 'Count'},
                    GroupTotalInstances: {units: 'Count'}
                },
                'AWS/CloudFront': {
                    Requests: {units: 'Count'},
                    BytesDownloaded: {units: 'Bytes'},
                    BytesUploaded: {units: 'Bytes'},
                    TotalErrorRate: {units: 'Percent'},
                    '4xxErrorRate': {units: 'Percent'},
                    '5xxErrorRate': {units: 'Percent'}
                },
                'AWS/EBS': {
                    VolumeReadBytes: {units: 'Bytes'},
                    VolumeWriteBytes: {units: 'Bytes'},
                    VolumeReadOps: {units: 'Count'},
                    VolumeWriteOps: {units: 'Count'},
                    VolumeTotalReadTime: {units: 'Seconds'},
                    VolumeTotalWriteTime: {units: 'Seconds'},
                    VolumeIdleTime: {units: 'Seconds'},
                    VolumeQueueLength: {units: 'Count'},
                    VolumeThroughputPercentage: {units: 'Percent'},
                    VolumeConsumedReadWriteOps: {units: 'Count'}
                },
                'AWS/EC2': {
                    DiskReadOps: {units: 'Count'},
                    DiskWriteOps: {units: 'Count'},
                    DiskReadBytes: {units: 'Bytes'},
                    DiskWriteBytes: {units: 'Bytes'},
                    CPUUtilization: {units: 'Percent'},
                    NetworkOut: {units: 'Bytes'},
                    NetworkIn: {units: 'Bytes'},
                    NetworkPacketsOut: {units: 'Count'},
                    NetworkPacketsIn: {units: 'Count'},
                    CPUCreditUsage: {units: 'Count'},
                    CPUCreditBalance: {units: 'Count'},
                    StatusCheckFailed_Instance: {units: 'Count'},
                    StatusCheckFailed_System: {units: 'Count'}
                },
                'AWS/ElastiCache': {
                    CPUUtilization: {units: 'Percent'},
                    FreeableMemory: {units: 'Bytes'},
                    NetworkBytesIn: {units: 'Bytes'},
                    NetworkBytesOut: {units: 'Bytes'},
                    SwapUsage: {units: 'Bytes'},
                    BytesUsedForCache: {units: 'Bytes'},
                    CacheHits: {units: 'Count'},
                    CacheMisses: {units: 'Count'},
                    CurrConnections: {units: 'Count'},
                    Evictions: {units: 'Count'},
                    HyperLogLogBasedCmds: {units: 'Count'},
                    NewConnections: {units: 'Count'},
                    Reclaimed: {units: 'Count'},
                    ReplicationBytes: {units: 'Bytes'},
                    ReplicationLag: {units: 'Seconds'},
                    SaveInProgress: {units: 'Count'},
                    CurrItems: {units: 'Count'},
                    GetTypeCmds: {units: 'Count'},
                    HashBasedCmds: {units: 'Count'},
                    KeyBasedCmds: {units: 'Count'},
                    ListBasedCmds: {units: 'Count'},
                    SetBasedCmds: {units: 'Count'},
                    SetTypeCmds: {units: 'Count'},
                    SortedSetBasedCmds: {units: 'Count'},
                    StringBasedCmds: {units: 'Count'},
                    BytesUsedForCacheItems: {units: 'Count'},
                    BytesReadIntoMemcached: {units: 'Bytes'},
                    BytesWrittenOutFromMemcached: {units: 'Bytes'},
                    CasBadval: {units: 'Count'},
                    CasHits: {units: 'Count'},
                    CasMisses: {units: 'Count'},
                    CmdFlush: {units: 'Count'},
                    CmdGet: {units: 'Count'},
                    CmdSet: {units: 'Count'},
                    DecrHits: {units: 'Count'},
                    DecrMisses: {units: 'Count'},
                    DeleteHits: {units: 'Count'},
                    DeleteMisses: {units: 'Count'},
                    GetHits: {units: 'Count'},
                    GetMisses: {units: 'Count'},
                    IncrHits: {units: 'Count'},
                    IncrMisses: {units: 'Count'},
                    BytesUsedForHash: {units: 'Bytes'},
                    CmdConfigGet: {units: 'Count'},
                    CmdConfigSet: {units: 'Count'},
                    CmdTouch: {units: 'Count'},
                    CurrConfig: {units: 'Count'},
                    EvictedUnfetched: {units: 'Count'},
                    ExpiredUnfetched: {units: 'Count'},
                    SlabsMoved: {units: 'Count'},
                    TouchHits: {units: 'Count'},
                    TouchMisses: {units: 'Count'},
                    NewItems: {units: 'Count'},
                    UnusedMemory: {units: 'Bytes'}
                },
                'AWS/ELB': {
                    HealthyHostCount: {units: 'Count'},
                    UnHealthyHostCount: {units: 'Count'},
                    RequestCount: {units: 'Count'},
                    Latency: {units: 'Seconds'},
                    HTTPCode_ELB_4XX: {units: 'Count'},
                    HTTPCode_ELB_5XX: {units: 'Count'},
                    HTTPCode_Backend_2XX: {units: 'Count'},
                    HTTPCode_Backend_3XX: {units: 'Count'},
                    HTTPCode_Backend_4XX: {units: 'Count'},
                    HTTPCode_Backend_5XX: {units: 'Count'},
                    BackendConnectionErrors: {units: 'Count'},
                    SurgeQueueLength: {units: 'Count'},
                    SpilloverCount: {units: 'Count'}
                },
                'AWS/Lambda': {
                    Invocations: {units: 'Count'},
                    Errors: {units: 'Count'},
                    Duration: {units: 'Milliseconds'},
                    Throttles: {units: 'Count'}
                },
                'AWS/RDS': {
                    BinLogDiskUsage: {units: 'Bytes'},
                    CPUUtilization: {units: 'Percent'},
                    CPUCreditUsage: {units: 'Count'},
                    CPUCreditBalance: {units: 'Count'},
                    DatabaseConnections: {units: 'Count'},
                    DiskQueueDepth: {units: 'Count'},
                    FreeableMemory: {units: 'Bytes'},
                    FreeStorageSpace: {units: 'Bytes'},
                    ReplicaLag: {units: 'Seconds'},
                    SwapUsage: {units: 'Bytes'},
                    ReadIOPS: {units: 'Count/Second'},
                    WriteIOPS: {units: 'Count/Second'},
                    ReadLatency: {units: 'Seconds'},
                    WriteLatency: {units: 'Seconds'},
                    ReadThroughput: {units: 'Bytes/Second'},
                    WriteThroughput: {units: 'Bytes/Second'},
                    NetworkReceiveThroughput: {units: 'Bytes/Second'},
                    NetworkTransmitThroughput: {units: 'Bytes/Second'},

                    // Not documented on RDS Cloudwatch API page, but
                    // adding here so validation will still pass
                    TransactionLogsDiskUsage: {units: 'Bytes'},
                    OldestReplicationSlotLag: {units: 'Bytes'}
                }
            };

            expect(point).to.contain.keys(['time', 'metric_type', 'product', 'namespace', 'name', 'dimension', 'item', 'statistic', 'value', 'units']);
            expect(point.metric_type).to.equal('AWS CloudWatch');
            expect(point.dimension).to.equal(expected_dimensions[point.product]);
            expect(_.keys(expected_dimensions)).to.include(point.product);

            // AutoScaling is slightly different--there can be
            // per-group metrics, in which case the namespace is
            // AWS/AutoScaling. Additionally, there are EC2-style
            // metrics for the group as a whole, which are returned
            // with the namespace AWS/EC2.
            if (point.product === 'AutoScaling') {
                expect(['AWS/AutoScaling', 'AWS/EC2']).to.include(point.namespace);
            } else {
                expect(point.namespace).to.equal(`AWS/${point.product}`);
            }

            // We allow metrics not in the above set--Cloudwatch adds
            // new metrics from time to time, so those shouldn't
            // result in test failures. We do log a warning, though.
            if (! _.contains(_.keys(expected_metrics[point.namespace]), point.name)) {
                console.log(`Unexpected Cloudwatch metric ${point.name}. Consider adding to set of supported metrics.`);
            } else {
                expect(statistics).to.include(point.statistic);
                expect(point.units).to.equal(expected_metrics[point.namespace][point.name].units);
            }
        }

        it('using defaults (all products, statistics=Average)', function() {
            return check_juttle({
                program: 'read cloudwatch | view text'
            })
            .then(function(result) {
                expect(result.errors).to.have.length(0);
                expect(result.warnings).to.have.length(0);
                for(let point of result.sinks.text) {
                    validate_point(point, ['Average']);
                }
            });
        });

        it('using EC2, statistics=Minimum', function() {
            return check_juttle({
                program: 'read cloudwatch -statistics ["Minimum"] product="EC2" | view text'
            })
            .then(function(result) {
                expect(result.errors).to.have.length(0);
                expect(result.warnings).to.have.length(0);
                for(let point of result.sinks.text) {
                    validate_point(point, ['Minimum']);
                }
            });
        });

        it('using EC2 and metric="CPUUtilization", statistics=Minimum', function() {
            return check_juttle({
                program: 'read cloudwatch -statistics ["Minimum"] product="EC2" AND metric="CPUUtilization" | view text'
            })
            .then(function(result) {
                expect(result.errors).to.have.length(0);
                expect(result.warnings).to.have.length(0);
                for(let point of result.sinks.text) {
                    validate_point(point, ['Minimum']);
                }
            });
        });

        it('using ELB, statistics=Average,Minimum', function() {
            return check_juttle({
                program: 'read cloudwatch -statistics ["Minimum", "Average"] product="ELB" | view text'
            })
            .then(function(result) {
                expect(result.errors).to.have.length(0);
                expect(result.warnings).to.have.length(0);
                // There should be an equal number of points for both statistics.
                let min_count = 0;
                let avg_count = 0;
                for(let point of result.sinks.text) {
                    validate_point(point, ['Minimum', 'Average']);
                    if (point.statistic === 'Minimum') {
                        min_count++;
                    } else {
                        avg_count++;
                    }
                }
                expect(min_count).to.equal(avg_count);
            });
        });

        it('with -period to get greater aggregations', function() {
            let num_default_points;
            return check_juttle({
                program: 'read cloudwatch -last :1 hour: product="EC2" | view text'
            })
            .then(function(result) {
                expect(result.errors).to.have.length(0);
                expect(result.warnings).to.have.length(0);
                num_default_points = result.sinks.text.length;
            })
            .then(function() {
                return check_juttle({
                    program: 'read cloudwatch -last :1 hour: -period 3600 product="EC2" | view text'
                });
            })
            .then(function(result) {
                expect(result.errors).to.have.length(0);
                expect(result.warnings).to.have.length(0);

                // We don't check for precisely 1/60 the number of
                // points, as some metrics like status checks, etc.
                // won't be included, and some instances might have
                // detailed monitoring enabled, both of which affect
                // the totals. 1/10 should be a safe threshold.
                expect(result.sinks.text.length).to.be.below(num_default_points*0.1);
            });
        });
    });
});


text:
<?xml version="1.0" encoding="utf-8"?>
  <browserconfig>
    <msapplication>
      <tile>
        <square70x70logo src="/path/to/smalltile.png"/>
        <square150x150logo src="/path/to/mediumtile.png"/>
        <wide310x150logo src="/path/to/widetile.png"/>
        <square310x310logo src="/path/to/largetile.png"/>
        <TileColor>#0078d7</TileColor>
      </tile>
    </msapplication>
  </browserconfig>


text:
namespace AVDump3Lib.Misc;

public class AppendLineManager : IDisposable {
	private readonly Dictionary<string, StreamWriter> streamWriters = new();
	private readonly Mutex mutex;

	public AppendLineManager() {
		mutex = new Mutex(false, "AVDump3-AppendLineManager-b569dae8-5002-47a3-beb0-bc98e52a3280");
	}

	public void AppendLine(string filePath, string line) {
		lock(streamWriters) {
			var mutexAquired = false;
			try {
				mutexAquired = mutex.WaitOne(10000);
			} catch(AbandonedMutexException) {
				mutexAquired = true;
				//May happen when a process halts unexpectedly
			}

			if(mutexAquired) {
				try {
					if(!streamWriters.TryGetValue(filePath, out var streamWriter)) {
						streamWriter = streamWriters[filePath] = new StreamWriter(File.Open(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite));
					}
					if(!string.IsNullOrEmpty(line)) {
						streamWriter.WriteLine(line);
						streamWriter.Flush();
					}
				} catch(Exception) {
					mutex.ReleaseMutex();
					throw;
				}
			} else {
				throw new Exception("Couldn't write to file in a timely manner") {
					Data = {
						{ "FilePath", new SensitiveData(filePath) }
					}
				};
			}
		}
	}

	public void Clear() {
		lock(streamWriters) {
			foreach(var streamWriter in streamWriters.Values) {
				streamWriter.Close();
			}
			streamWriters.Clear();
		}
	}

	public void Dispose() {
		mutex.Dispose();
		GC.SuppressFinalize(this);
	}
}

/*
namespace AVDump3Lib.Misc;

public class AppendLineManager {
	private readonly Dictionary<string, StreamWriter> streamWriters = new();

	public void AppendLine(string filePath, string line) {
		lock(streamWriters) {
			for(int i = 10; i == 0; i--) {
				try {
					if(!streamWriters.TryGetValue(filePath, out var streamWriter)) {
						streamWriter = streamWriters[filePath] = new StreamWriter(File.Open(filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite));
					}
					if(!string.IsNullOrEmpty(line)) {
						streamWriter.WriteLine(line);
						streamWriter.Flush();
					}
					break;

				} catch(Exception) {
					if(i == 0) throw;
					Thread.Sleep(100);
				}
			}
		}
	}

	public void Clear() {
		lock(streamWriters) {
			foreach(var streamWriter in streamWriters.Values) {
				streamWriter.Close();
			}
			streamWriters.Clear();
		}
	}
}

 */


text:
<?php

return array(
    'phpSettings' => array(
        'display_startup_errors' => false,
        'display_errors' => false,
        'date' => array(
            'timezone' => 'America/New_York'
        )
    ),
    'bootstrap' => array(
        'path' => APPLICATION_PATH . '/Bootstrap.php'
    ),
    'autoloadernamespaces' => array(
        'Doctrine\\'
    ),
    'resources' => array(
        'frontController' => array(
            'moduleDirectory' => APPLICATION_PATH,
            'env' => APPLICATION_ENV,
            'actionHelperPaths' => array(
                'Epixa\\Controller\\Helper\\' => 'Epixa/Controller/Helper'
            )
        ),
        'doctrine' => array(
            'loggerClass' => 'Doctrine\\DBAL\\Logging\\EchoSQLLogger',
            'proxy' => array(
                'directory' => APPLICATION_ROOT . '/data/proxies'
            ),
            'connection'   => array(
                'driver'   => 'pdo_mysql',
                'host'     => 'localhost',
                'dbname'   => 'basedb',
                'user'     => 'productionusr',
                'password' => 'productionpass'
            )
        ),
        'modules' => array(),
        'router' => array(
            'file' => APPLICATION_ROOT . '/config/routes.php'
        ),
        'view' => array(),
        'layout' => array(
            'layoutPath' => APPLICATION_ROOT . '/layouts',
            'layout' => 'default'
        )
    )
);


text:
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">×</button>

Users settings

            Name
            WIP
            Order
            Visible

                <button type="button" class="btn btn-xs btn-default" data-bind="click: $parent.orderUp.bind($parent)">

                </button>
                <button type="button" class="btn btn-xs btn-default" data-bind="click: $parent.orderDown.bind($parent)">

                </button>

              <input type="checkbox" data-bind="checked: isVisible, click: $parent.visibleUser.bind($parent)" />

        <form class="form">

            <input type="text" class="form-control" id="add-username" placeholder="User Name"
                   data-bind="value: addUsername" required>

                <button class="btn btn-default btn-success" type="submit" data-bind="click: addUser">
                    Add
                </button>

        </form>


text:
<md-dialog aria-label="{{ 'Health Check' | translate }}"  ng-cloak>
  <form name="myForm">
    <md-toolbar>

Update SSID

    </md-toolbar>
    <md-dialog-content class="ssid-form">

Changing the SSID will resync your boxes.

          <md-input-container>

              <translate>SSID</translate>

            <input name="ssid" md-maxlength="32" ng-model="networkEdit.ssid" emoji-picker="{content: '.ssid-input', pickerPosition: 'bottom'}" class="ssid-input" ng-pattern="/^[^[\]\'\\]+$/" ssid required>

Max 32 characters

You must set an SSID

Invalid SSID

SSID is too large, emojis take up more space!

          </md-input-container>

    </md-dialog-content>
    <md-dialog-actions layout="row">

      <md-button ng-click="close()">
        <translate>
          Close
        </translate>
      </md-button>
      <md-button class="md-raised" ng-disabled="myForm.$pristine || myForm.$invalid" ng-click="update()">
        <translate>
          UPDATE
        </translate>
      </md-button>
    </md-dialog-actions>
  </form>
</md-dialog>


text:
<?php

use yii\db\Schema;
use yii\db\Migration;

class m150726_123441_create_user_table extends Migration {
	public function up() {
		$tableOptions = null;
		if ($this->db->driverName === 'mysql') {
			$tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';
		}

		$this->createTable('{{%user}}', [
			'id'                   => Schema::TYPE_PK,
			'created_at'           => Schema::TYPE_INTEGER . ' NOT NULL',
			'updated_at'           => Schema::TYPE_INTEGER . ' NOT NULL',
			'username'             => Schema::TYPE_STRING . ' NOT NULL',
			'auth_key'             => Schema::TYPE_STRING . '(32) NULL DEFAULT NULL',
			'email_confirm_token'  => Schema::TYPE_STRING . ' NULL DEFAULT NULL',
			'password_hash'        => Schema::TYPE_STRING . ' NOT NULL',
			'password_reset_token' => Schema::TYPE_STRING . ' NULL DEFAULT NULL',
			'email'                => Schema::TYPE_STRING . ' NOT NULL',
			'status'               => Schema::TYPE_SMALLINT . ' NOT NULL DEFAULT 0',
		], $tableOptions);

		$this->createIndex('idx_user_username', '{{%user}}', 'username');
		$this->createIndex('idx_user_email', '{{%user}}', 'email');
		$this->createIndex('idx_user_status', '{{%user}}', 'status');
	}

	public function down() {
		echo "m150724_212831_create_user_table dropTable user.\n";
		$this->dropTable('{{%user}}');
	}

	/*
	// Use safeUp/safeDown to run migration code within a transaction
	public function safeUp()
	{
	}

	public function safeDown()
	{
	}
	*/
}


text:
<?php

namespace sitemaps;

final class HelperTools {
	// ISO definition for datetime-string
	const ISO8601='Y-m-d\TH:i:sP';

	// HelperTools should not be avaible for anything
	protected function __construct() {}
	protected function __clone() {}

	static public function getISO8601TimeString($time) {
		return date(self::ISO8601, $time);
	}

	static public function LocationValidString($string) {
		$string=str_replace(array("&", "\'", "\"", ">", "<"), array("&", "'", """, ">", "<"), $string);

		return $string;
	}
}


text:
"use strict";

module.exports.status = function status(req, res, next) {
    res.json({"status": "ok"});
    return next();
};


text: CKEDITOR.plugins.setLang("print","sq",{toolbar:"Shtype"});


text:
<?xml version="1.0" ?>
<srv:container xmlns="http://doctrine-project.org/schemas"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                        http://doctrine-project.org/schemas http://doctrine-project.org/schemas/doctrine_cache-1.0.xsd">

     <doctrine-cache>
        <provider name="foo_namespace_provider" type="array"    namespace="foo_namespace"/>
        <provider name="barNamespaceProvider"   type="array"    namespace="barNamespace"/>
    </doctrine-cache>
</srv:container>


text:
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
             xmlns:g="urn:import:com.google.gwt.user.client.ui"
             xmlns:btn="urn:import:stroom.widget.button.client">
    <g:FlowPanel styleName="dataNavigator">

        <g:Label ui:field="lblParts" styleName="dataNavigator-label"/>
        <btn:SvgButton ui:field="firstPart"/>
        <btn:SvgButton ui:field="prevPart"/>
        <btn:SvgButton ui:field="nextPart"/>
        <btn:SvgButton ui:field="lastPart"/>

        <g:Label ui:field="lblSegments" styleName="dataNavigator-label"/>
        <btn:SvgButton ui:field="firstSegment"/>
        <btn:SvgButton ui:field="prevSegment"/>
        <btn:SvgButton ui:field="nextSegment"/>
        <btn:SvgButton ui:field="lastSegment"/>

        <g:Label ui:field="lblCharacters" styleName="dataNavigator-label"/>
        <btn:SvgButton ui:field="showHeadCharactersBtn"/>
        <btn:SvgButton ui:field="advanceCharactersBackwardBtn"/>
        <btn:SvgButton ui:field="advanceCharactersForwardBtn"/>

        <btn:SvgButton ui:field="refresh"/>
    </g:FlowPanel>
</ui:UiBinder>


text:
package com.palantir.atlasdb.timelock.stress;

import static com.palantir.atlasdb.timelock.TemplateVariables.generateThreeNodeTimelockCluster;

import com.github.peterwippermann.junit4.parameterizedsuite.ParameterizedSuite;
import com.google.common.collect.ImmutableSet;
import com.palantir.atlasdb.timelock.MultiNodePaxosTimeLockServerStressTest;
import com.palantir.atlasdb.timelock.TestableTimelockCluster;
import com.palantir.atlasdb.timelock.util.TestableTimeLockClusterPorts;
import com.palantir.timelock.config.PaxosInstallConfiguration.PaxosLeaderMode;
import java.util.Collection;
import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Suite;

@RunWith(ParameterizedSuite.class)
@Suite.SuiteClasses(MultiNodePaxosTimeLockServerStressTest.class)
public final class SingleLeaderPaxosStressTests {

    public static final TestableTimelockCluster NON_BATCHED_TIMESTAMP_PAXOS = new TestableTimelockCluster(
            "non-batched timestamp paxos single leader",
            "paxosMultiServer.ftl",
            generateThreeNodeTimelockCluster(
                    TestableTimeLockClusterPorts.SINGLE_LEADER_PAXOS_STRESS_TESTS_1,
                    builder -> builder.clientPaxosBuilder(
                                    builder.clientPaxosBuilder().isUseBatchPaxosTimestamp(false))
                            .leaderMode(PaxosLeaderMode.SINGLE_LEADER)));

    public static final TestableTimelockCluster BATCHED_TIMESTAMP_PAXOS = new TestableTimelockCluster(
            "batched timestamp paxos single leader",
            "paxosMultiServer.ftl",
            generateThreeNodeTimelockCluster(
                    TestableTimeLockClusterPorts.SINGLE_LEADER_PAXOS_STRESS_TESTS_2,
                    builder -> builder.clientPaxosBuilder(
                                    builder.clientPaxosBuilder().isUseBatchPaxosTimestamp(true))
                            .leaderMode(PaxosLeaderMode.SINGLE_LEADER)));

    public static final TestableTimelockCluster BATCHED_PAXOS = new TestableTimelockCluster(
            "batched single leader",
            "paxosMultiServer.ftl",
            generateThreeNodeTimelockCluster(
                    TestableTimeLockClusterPorts.SINGLE_LEADER_PAXOS_STRESS_TESTS_3,
                    builder -> builder.clientPaxosBuilder(builder.clientPaxosBuilder()
                                    .isUseBatchPaxosTimestamp(false)
                                    .isBatchSingleLeader(true))
                            .leaderMode(PaxosLeaderMode.SINGLE_LEADER)));

    @Parameterized.Parameters(name = "{0}")
    public static Collection<TestableTimelockCluster> params() {
        return ImmutableSet.of(NON_BATCHED_TIMESTAMP_PAXOS, BATCHED_TIMESTAMP_PAXOS, BATCHED_PAXOS);
    }

    @Rule
    @Parameterized.Parameter
    public TestableTimelockCluster cluster;
}


text:
package org.bouncycastle.math.ec.endo;

import org.bouncycastle.math.ec.ECPoint;
import org.bouncycastle.math.ec.PreCompInfo;

public class EndoPreCompInfo implements PreCompInfo
{
    protected ECEndomorphism endomorphism;

    protected ECPoint mappedPoint;

    public ECEndomorphism getEndomorphism()
    {
        return endomorphism;
    }

    public void setEndomorphism(ECEndomorphism endomorphism)
    {
        this.endomorphism = endomorphism;
    }

    public ECPoint getMappedPoint()
    {
        return mappedPoint;
    }

    public void setMappedPoint(ECPoint mappedPoint)
    {
        this.mappedPoint = mappedPoint;
    }
}


text:
"""
Database

Stores all the tracks contained within the iPod indexed in a convenient way for
fast retrieval, iteration, addition and removal.

Intended as a very thin wrapper around gpod.Database in order to provide a
cleaner interface, extra functionality and resolve some flaws.

    # Create the database without indexing its contents
    database = ipodio.Database(gpod.Database('/ipod/mountpoint'))

    # Index all tracks by its hash.
    # The chosen structure is a dictionary of sets,
    # which might be regarded as # a multi-dict.
    database.update_index()

    # The indexing allows to get a database by its inner contents
    # and thus making it easy to detect and avoid duplication.
    database.get_by_hash('some_calculated_track_hash')

    # Basic operations along with a ipodio.Track instance
    database.get(track)
    database.add(track)
    database.remove(track)

    # The gpod Database reference is public until those use cases which need it
    # are sorted out and implemented as part of the class.
    database.internal

    # A flag is maintained and updated on database modification
    # so unnecessary expensive closing work is spared by checking.
    if database.updated:
        database.copy_files()   # Physically send track files if needed
        database.save()         # Save the current database state and store it
"""

import gpod

from collections import defaultdict

from .track import Track

def first(iterable):
    for item in iterable:
        return item

class Playlist(object):
    def __init__(self, playlist):
        self.__playlist = playlist

    @classmethod
    def create(cls, name, database, internal_class=gpod.Playlist):
        return cls(internal_class(database.internal, name))

    @property
    def internal(self):
        return self.__playlist

    @property
    def name(self):
        return self.__playlist.get_name()

    @name.setter
    def name(self, name):
        self.__playlist.set_name(name)

    @property
    def is_master(self):
        return self.__playlist.get_master()

    @property
    def tracks(self):
        return [Track(track) for track in self.__playlist]

    def append(self, track):
        self.__playlist.add(track.internal)

    def extend(self, tracks):
        map(self.append, tracks)

    def remove(self, track):
        self.__playlist.remove(track.internal)

    def discard(self, tracks):
        map(self.remove, tracks)

class Database(object):
    def __init__(self, database):
        self.__database = database
        self.index = defaultdict(set)
        self.updated = False

    @classmethod
    def create(cls, mountpoint, internal_class=gpod.Database):
        return cls(internal_class(mountpoint))

    @property
    def internal(self):
        return self.__database

    @property
    def tracks(self):
        return [Track(track) for track in self.__database]

    @property
    def playlists(self):
        return [Playlist(playlist) for playlist in self.__database.Playlists]

    def __add_index(self, track):
        if not track.hash:
            self.updated = True
            track.update_hash()

        self.index[track.hash].add(track)

    def update_index(self):
        for track in self.tracks:
            self.__add_index(track)

    def get(self, track):
        return self.get_by_hash(track.hash)

    def get_by_hash(self, hash):
        return first(self.find_by_hash(hash))

    def find_by_hash(self, hash):
        return self.index[hash]

    def add(self, track):
        self.updated = True
        self.__add_index(track)
        self.__database.add(track.internal)
        self.__database.Master.add(track.internal)

    @property
    def duplicates(self):
        return [group for group in self.index.itervalues() if len(group) > 1]

    def remove(self, track):
        self.updated = True
        self.__database.remove(track.internal, quiet=True)

    def remove_playlist(self, playlist):
        self.updated = True
        # Avoid physically removing tracks from the iPod by setting ipod=False
        # This may orphan tracks if they were only in this playlist
        self.__database.remove(playlist.internal, quiet=True, ipod=False)

    def copy_files(self, progress=None):
        self.__database.copy_delayed_files(progress)

    def save(self):
        self.__database.close()


text:
<?php

use app\models\general\GeneralLabel;

use yii\helpers\Html;
use yii\grid\GridView;

/* @var $this yii\web\View */
/* @var $searchModel frontend\models\AkkSijilPertolonganCemasSearch */
/* @var $dataProvider yii\data\ActiveDataProvider */

$this->title = GeneralLabel::akk_sijil_pertolongan_cemas;
$this->params['breadcrumbs'][] = $this->title;
?>

<?= Html::encode($this->title) ?>

    <?php // echo $this->render('_search', ['model' => $searchModel]); ?>

        <?= Html::a(GeneralLabel::create.' '.GeneralLabel::akk_sijil_pertolongan_cemas, ['create'], ['class' => 'btn btn-success']) ?>

    <?= GridView::widget([
        'dataProvider' => $dataProvider,
        'filterModel' => $searchModel,
        'columns' => [
            ['class' => 'yii\grid\SerialColumn'],

            'akk_sijil_pertolongan_cemas_id',
            [
                'attribute' => 'akademi_akk_id',
                'filterInputOptions' => [
                    'class'       => 'form-control',
                    'placeholder' => GeneralLabel::filter.' '.GeneralLabel::akademi_akk_id,
                ]
            ],
            [
                'attribute' => 'no_sijil',
                'filterInputOptions' => [
                    'class'       => 'form-control',
                    'placeholder' => GeneralLabel::filter.' '.GeneralLabel::no_sijil,
                ]
            ],
            [
                'attribute' => 'tahap',
                'filterInputOptions' => [
                    'class'       => 'form-control',
                    'placeholder' => GeneralLabel::filter.' '.GeneralLabel::tahap,
                ]
            ],
            [
                'attribute' => 'tahun',
                'filterInputOptions' => [
                    'class'       => 'form-control',
                    'placeholder' => GeneralLabel::filter.' '.GeneralLabel::tahun,
                ]
            ],
            // 'sijil',
            // 'session_id',
            // 'created_by',
            // 'updated_by',
            // 'created',
            // 'updated',

            ['class' => 'yii\grid\ActionColumn'],
        ],
    ]); ?>


text:
<?php

namespace KMT\AAShop\DependencyInjection;

use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;

/**
 * Sylius frontend DIC extension
 *
 * @author Paweł Jędrzejewski <pawel@sylius.org>
 */
class KMTAAShopExtension extends Extension
{
    /**
     * {@inheritdoc}
     */
    public function load(array $config, ContainerBuilder $container)
    {
        $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('services.xml');
        //$loader->load('twig.xml');
    }
}


text:
package com.mongodash.model;

public class Breadcrumb {

	String title;
	String link;

	public Breadcrumb(String title, String link) {
		this.title = title;
		this.link = link;
	}

	public Breadcrumb(String title) {
		this.title = title;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getLink() {
		return link;
	}

	public void setLink(String link) {
		this.link = link;
	}

}


text:
try:
    from setuptools import setup
except ImportError:
    from distutils.core import setup

readme = open('README.rst').read()
history = open('HISTORY.rst').read().replace('.. :changelog:', '')

setup(
    name='polygamy',

    version='0.1.2',
    description='Handle multiple SCM repositories easily.',
    long_description=readme + '\n\n' + history,
    author='Chris Trotman',
    author_email='chris@trotman.io',
    url='https://github.com/solarnz/polygamy',
    packages=[
        'polygamy',
    ],
    include_package_data=True,
    install_requires=[
        'blessings == 1.5.1',
        'gevent >= 1.0',
        'tabulate == 0.7.2',
    ],
    license="BSD",
    zip_safe=False,
    keywords='polygamy',
    classifiers=[
        'Development Status :: 2 - Pre-Alpha',
        'Intended Audience :: Developers',
        'License :: OSI Approved :: BSD License',
        'Natural Language :: English',
        "Programming Language :: Python :: 2",
        'Programming Language :: Python :: 2.7',
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.3',
        'Programming Language :: Python :: 3.4',
    ],
    test_suite='tests',
    entry_points={
        'console_scripts': [
            'polygamy = polygamy:main'
        ]
    },
)