2014-06-07 19:07:54 +00:00
// +build gotask
package gotasks
2013-10-06 21:06:47 +00:00
import (
"archive/zip"
"bytes"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path"
"path/filepath"
"regexp"
"strings"
2014-06-07 19:07:54 +00:00
"text/template"
2013-10-06 21:06:47 +00:00
"github.com/huin/goupnp"
"github.com/huin/goupnp/scpd"
2014-01-12 01:10:05 +00:00
"github.com/huin/goutil/codegen"
2014-06-07 19:07:54 +00:00
"github.com/jingweno/gotask/tasking"
2013-10-06 21:06:47 +00:00
)
var (
deviceURNPrefix = "urn:schemas-upnp-org:device:"
serviceURNPrefix = "urn:schemas-upnp-org:service:"
)
2014-06-07 19:07:54 +00:00
// NAME
// specgen - generates Go code from the UPnP specification files.
//
// DESCRIPTION
// The specification is available for download from:
//
// OPTIONS
// -s, --spec_filename=<upnpresources.zip>
// Path to the specification file, available from http://upnp.org/resources/upnpresources.zip
// -o, --out_dir=<output directory>
2014-06-07 19:34:27 +00:00
// Path to the output directory. This is is where the DCP source files will be placed. Should normally correspond to the directory for github.com/huin/goupnp/dcps
2014-06-07 19:07:54 +00:00
// --nogofmt
// Disable passing the output through gofmt. Do this if debugging code output problems and needing to see the generated code prior to being passed through gofmt.
func TaskSpecgen ( t * tasking . T ) {
specFilename := t . Flags . String ( "spec-filename" )
if specFilename == "" {
specFilename = t . Flags . String ( "s" )
}
if specFilename == "" {
t . Fatal ( "--spec_filename is required" )
}
outDir := t . Flags . String ( "out-dir" )
if outDir == "" {
outDir = t . Flags . String ( "o" )
}
if outDir == "" {
log . Fatal ( "--out_dir is required" )
}
useGofmt := ! t . Flags . Bool ( "nogofmt" )
specArchive , err := openZipfile ( specFilename )
2013-10-06 21:06:47 +00:00
if err != nil {
2014-06-07 19:07:54 +00:00
t . Fatalf ( "Error opening spec file: %v" , err )
2013-10-06 21:06:47 +00:00
}
defer specArchive . Close ( )
2014-06-07 19:55:03 +00:00
dcpCol := newDcpsCollection ( )
2013-10-06 21:06:47 +00:00
for _ , f := range globFiles ( "standardizeddcps/*/*.zip" , specArchive . Reader ) {
dirName := strings . TrimPrefix ( f . Name , "standardizeddcps/" )
slashIndex := strings . Index ( dirName , "/" )
if slashIndex == - 1 {
// Should not happen.
2014-06-07 19:07:54 +00:00
t . Logf ( "Could not find / in %q" , dirName )
2013-10-06 21:06:47 +00:00
return
}
dirName = dirName [ : slashIndex ]
2014-06-07 19:34:27 +00:00
dcp := dcpCol . dcpForDir ( dirName )
2013-10-06 21:06:47 +00:00
if dcp == nil {
2014-06-07 19:07:54 +00:00
t . Logf ( "No alias defined for directory %q: skipping %s\n" , dirName , f . Name )
2013-10-06 21:06:47 +00:00
continue
2014-06-07 19:07:54 +00:00
} else {
t . Logf ( "Alias found for directory %q: processing %s\n" , dirName , f . Name )
2013-10-06 21:06:47 +00:00
}
dcp . processZipFile ( f )
}
2014-06-07 19:34:27 +00:00
for _ , dcp := range dcpCol . dcpByAlias {
2014-06-07 19:07:54 +00:00
if err := dcp . writePackage ( outDir , useGofmt ) ; err != nil {
2014-06-07 19:29:11 +00:00
log . Printf ( "Error writing package %q: %v" , dcp . Metadata . Name , err )
2013-10-06 21:06:47 +00:00
}
}
}
2014-06-07 20:24:11 +00:00
// DCP contains extra metadata to use when generating DCP source files.
2014-06-07 19:34:27 +00:00
type DCPMetadata struct {
2014-06-07 20:24:11 +00:00
Name string // What to name the Go DCP package.
OfficialName string // Official name for the DCP.
DocURL string // Optional - URL for futher documentation about the DCP.
2014-06-07 19:29:11 +00:00
}
2014-06-07 19:55:03 +00:00
var dcpMetadataByDir = map [ string ] DCPMetadata {
2014-06-07 20:24:11 +00:00
"Internet Gateway_1" : {
Name : "internetgateway1" ,
OfficialName : "Internet Gateway Device v1" ,
DocURL : "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf" ,
} ,
"Internet Gateway_2" : {
Name : "internetgateway2" ,
OfficialName : "Internet Gateway Device v2" ,
DocURL : "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf" ,
} ,
2014-06-07 19:55:03 +00:00
}
2014-06-07 19:34:27 +00:00
type dcpCollection struct {
2014-06-07 19:55:03 +00:00
dcpByAlias map [ string ] * DCP
2013-10-06 21:06:47 +00:00
}
2014-06-07 19:55:03 +00:00
func newDcpsCollection ( ) * dcpCollection {
2014-06-07 19:34:27 +00:00
c := & dcpCollection {
2014-06-07 19:55:03 +00:00
dcpByAlias : make ( map [ string ] * DCP ) ,
2013-10-06 21:06:47 +00:00
}
2014-06-07 19:34:27 +00:00
for _ , metadata := range dcpMetadataByDir {
c . dcpByAlias [ metadata . Name ] = newDCP ( metadata )
2013-10-06 21:06:47 +00:00
}
return c
}
2014-06-07 19:34:27 +00:00
func ( c * dcpCollection ) dcpForDir ( dirName string ) * DCP {
2014-06-07 19:55:03 +00:00
metadata , ok := dcpMetadataByDir [ dirName ]
2013-10-06 21:06:47 +00:00
if ! ok {
return nil
}
2014-06-07 19:34:27 +00:00
return c . dcpByAlias [ metadata . Name ]
2013-10-06 21:06:47 +00:00
}
// DCP collects together information about a UPnP Device Control Protocol.
type DCP struct {
2014-06-07 19:34:27 +00:00
Metadata DCPMetadata
2013-10-06 21:06:47 +00:00
DeviceTypes map [ string ] * URNParts
ServiceTypes map [ string ] * URNParts
Services [ ] SCPDWithURN
}
2014-06-07 19:34:27 +00:00
func newDCP ( metadata DCPMetadata ) * DCP {
2013-10-06 21:06:47 +00:00
return & DCP {
2014-06-07 19:29:11 +00:00
Metadata : metadata ,
2013-10-06 21:06:47 +00:00
DeviceTypes : make ( map [ string ] * URNParts ) ,
ServiceTypes : make ( map [ string ] * URNParts ) ,
}
}
func ( dcp * DCP ) processZipFile ( file * zip . File ) {
archive , err := openChildZip ( file )
if err != nil {
log . Println ( "Error reading child zip file:" , err )
return
}
for _ , deviceFile := range globFiles ( "*/device/*.xml" , archive ) {
dcp . processDeviceFile ( deviceFile )
}
for _ , scpdFile := range globFiles ( "*/service/*.xml" , archive ) {
dcp . processSCPDFile ( scpdFile )
}
}
func ( dcp * DCP ) processDeviceFile ( file * zip . File ) {
var device goupnp . Device
if err := unmarshalXmlFile ( file , & device ) ; err != nil {
log . Printf ( "Error decoding device XML from file %q: %v" , file . Name , err )
return
}
device . VisitDevices ( func ( d * goupnp . Device ) {
t := strings . TrimSpace ( d . DeviceType )
if t != "" {
u , err := extractURNParts ( t , deviceURNPrefix )
if err != nil {
log . Println ( err )
return
}
dcp . DeviceTypes [ t ] = u
}
} )
device . VisitServices ( func ( s * goupnp . Service ) {
u , err := extractURNParts ( s . ServiceType , serviceURNPrefix )
if err != nil {
log . Println ( err )
return
}
dcp . ServiceTypes [ s . ServiceType ] = u
} )
}
2014-06-07 19:07:54 +00:00
func ( dcp * DCP ) writePackage ( outDir string , useGofmt bool ) error {
2014-06-07 19:29:11 +00:00
packageDirname := filepath . Join ( outDir , dcp . Metadata . Name )
2013-10-06 21:06:47 +00:00
err := os . MkdirAll ( packageDirname , os . ModePerm )
if err != nil && ! os . IsExist ( err ) {
return err
}
2014-06-07 19:29:11 +00:00
packageFilename := filepath . Join ( packageDirname , dcp . Metadata . Name + ".go" )
2013-10-06 21:06:47 +00:00
packageFile , err := os . Create ( packageFilename )
if err != nil {
return err
}
2014-01-06 19:47:57 +00:00
var output io . WriteCloser = packageFile
2014-06-07 19:07:54 +00:00
if useGofmt {
2014-01-12 01:10:05 +00:00
if output , err = codegen . NewGofmtWriteCloser ( output ) ; err != nil {
2014-01-06 19:47:57 +00:00
packageFile . Close ( )
return err
}
}
if err = packageTmpl . Execute ( output , dcp ) ; err != nil {
output . Close ( )
return err
}
return output . Close ( )
}
2013-10-06 21:06:47 +00:00
func ( dcp * DCP ) processSCPDFile ( file * zip . File ) {
scpd := new ( scpd . SCPD )
if err := unmarshalXmlFile ( file , scpd ) ; err != nil {
log . Printf ( "Error decoding SCPD XML from file %q: %v" , file . Name , err )
return
}
scpd . Clean ( )
urnParts , err := urnPartsFromSCPDFilename ( file . Name )
if err != nil {
log . Printf ( "Could not recognize SCPD filename %q: %v" , file . Name , err )
return
}
dcp . Services = append ( dcp . Services , SCPDWithURN {
URNParts : urnParts ,
SCPD : scpd ,
} )
}
type SCPDWithURN struct {
* URNParts
SCPD * scpd . SCPD
}
2013-12-31 17:52:43 +00:00
func ( s * SCPDWithURN ) WrapArgument ( arg scpd . Argument ) ( * argumentWrapper , error ) {
2013-12-31 16:56:20 +00:00
relVar := s . SCPD . GetStateVariable ( arg . RelatedStateVariable )
if relVar == nil {
return nil , fmt . Errorf ( "no such state variable: %q, for argument %q" , arg . RelatedStateVariable , arg . Name )
}
cnv , ok := typeConvs [ relVar . DataType . Name ]
if ! ok {
return nil , fmt . Errorf ( "unknown data type: %q, for state variable %q, for argument %q" , relVar . DataType . Type , arg . RelatedStateVariable , arg . Name )
}
return & argumentWrapper {
Argument : arg ,
relVar : relVar ,
conv : cnv ,
} , nil
}
type argumentWrapper struct {
scpd . Argument
relVar * scpd . StateVariable
conv conv
}
func ( arg * argumentWrapper ) AsParameter ( ) string {
return fmt . Sprintf ( "%s %s" , arg . Name , arg . conv . ExtType )
}
2013-12-31 17:52:43 +00:00
func ( arg * argumentWrapper ) Document ( ) string {
relVar := arg . relVar
if rng := relVar . AllowedValueRange ; rng != nil {
var parts [ ] string
if rng . Minimum != "" {
parts = append ( parts , fmt . Sprintf ( "minimum=%s" , rng . Minimum ) )
}
if rng . Maximum != "" {
parts = append ( parts , fmt . Sprintf ( "maximum=%s" , rng . Maximum ) )
}
if rng . Step != "" {
parts = append ( parts , fmt . Sprintf ( "step=%s" , rng . Step ) )
}
return "allowed value range: " + strings . Join ( parts , ", " )
}
if len ( relVar . AllowedValues ) != 0 {
return "allowed values: " + strings . Join ( relVar . AllowedValues , ", " )
}
return ""
}
2013-12-31 16:56:20 +00:00
func ( arg * argumentWrapper ) Marshal ( ) string {
return fmt . Sprintf ( "soap.Marshal%s(%s)" , arg . conv . FuncSuffix , arg . Name )
}
func ( arg * argumentWrapper ) Unmarshal ( objVar string ) string {
return fmt . Sprintf ( "soap.Unmarshal%s(%s.%s)" , arg . conv . FuncSuffix , objVar , arg . Name )
}
type conv struct {
FuncSuffix string
ExtType string
}
// typeConvs maps from a SOAP type (e.g "fixed.14.4") to the function name
// suffix inside the soap module (e.g "Fixed14_4") and the Go type.
var typeConvs = map [ string ] conv {
"ui1" : conv { "Ui1" , "uint8" } ,
"ui2" : conv { "Ui2" , "uint16" } ,
"ui4" : conv { "Ui4" , "uint32" } ,
"i1" : conv { "I1" , "int8" } ,
"i2" : conv { "I2" , "int16" } ,
"i4" : conv { "I4" , "int32" } ,
"int" : conv { "Int" , "int64" } ,
"r4" : conv { "R4" , "float32" } ,
"r8" : conv { "R8" , "float64" } ,
"number" : conv { "R8" , "float64" } , // Alias for r8.
"fixed.14.4" : conv { "Fixed14_4" , "float64" } ,
"float" : conv { "R8" , "float64" } ,
"char" : conv { "Char" , "rune" } ,
"string" : conv { "String" , "string" } ,
"date" : conv { "Date" , "time.Time" } ,
"dateTime" : conv { "DateTime" , "time.Time" } ,
"dateTime.tz" : conv { "DateTimeTz" , "time.Time" } ,
"time" : conv { "TimeOfDay" , "soap.TimeOfDay" } ,
"time.tz" : conv { "TimeOfDayTz" , "soap.TimeOfDay" } ,
"boolean" : conv { "Boolean" , "bool" } ,
"bin.base64" : conv { "BinBase64" , "[]byte" } ,
"bin.hex" : conv { "BinHex" , "[]byte" } ,
}
2013-10-06 21:06:47 +00:00
type closeableZipReader struct {
io . Closer
* zip . Reader
}
func openZipfile ( filename string ) ( * closeableZipReader , error ) {
file , err := os . Open ( filename )
if err != nil {
return nil , err
}
fi , err := file . Stat ( )
if err != nil {
return nil , err
}
archive , err := zip . NewReader ( file , fi . Size ( ) )
if err != nil {
return nil , err
}
return & closeableZipReader {
Closer : file ,
Reader : archive ,
} , nil
}
// openChildZip opens a zip file within another zip file.
func openChildZip ( file * zip . File ) ( * zip . Reader , error ) {
zipFile , err := file . Open ( )
if err != nil {
return nil , err
}
defer zipFile . Close ( )
zipBytes , err := ioutil . ReadAll ( zipFile )
if err != nil {
return nil , err
}
return zip . NewReader ( bytes . NewReader ( zipBytes ) , int64 ( len ( zipBytes ) ) )
}
func globFiles ( pattern string , archive * zip . Reader ) [ ] * zip . File {
var files [ ] * zip . File
for _ , f := range archive . File {
if matched , err := path . Match ( pattern , f . Name ) ; err != nil {
// This shouldn't happen - all patterns are hard-coded, errors in them
// are a programming error.
panic ( err )
} else if matched {
files = append ( files , f )
}
}
return files
}
func unmarshalXmlFile ( file * zip . File , data interface { } ) error {
r , err := file . Open ( )
if err != nil {
return err
}
decoder := xml . NewDecoder ( r )
r . Close ( )
return decoder . Decode ( data )
}
type URNParts struct {
URN string
Name string
Version string
}
func ( u * URNParts ) Const ( ) string {
return fmt . Sprintf ( "URN_%s_%s" , u . Name , u . Version )
}
// extractURNParts extracts the name and version from a URN string.
func extractURNParts ( urn , expectedPrefix string ) ( * URNParts , error ) {
if ! strings . HasPrefix ( urn , expectedPrefix ) {
return nil , fmt . Errorf ( "%q does not have expected prefix %q" , urn , expectedPrefix )
}
parts := strings . SplitN ( strings . TrimPrefix ( urn , expectedPrefix ) , ":" , 2 )
if len ( parts ) != 2 {
return nil , fmt . Errorf ( "%q does not have a name and version" , urn )
}
name , version := parts [ 0 ] , parts [ 1 ]
return & URNParts { urn , name , version } , nil
}
var scpdFilenameRe = regexp . MustCompile (
` .*/([a-zA-Z0-9]+)([0-9]+)\.xml ` )
func urnPartsFromSCPDFilename ( filename string ) ( * URNParts , error ) {
parts := scpdFilenameRe . FindStringSubmatch ( filename )
if len ( parts ) != 3 {
return nil , fmt . Errorf ( "SCPD filename %q does not have expected number of parts" , filename )
}
name , version := parts [ 1 ] , parts [ 2 ]
return & URNParts {
URN : serviceURNPrefix + name + ":" + version ,
Name : name ,
Version : version ,
} , nil
}
2014-06-07 19:07:54 +00:00
2014-06-07 19:29:11 +00:00
var packageTmpl = template . Must ( template . New ( "package" ) . Parse ( ` { { $ name := . Metadata . Name } }
2014-06-07 20:24:11 +00:00
// Client for UPnP Device Control Protocol {{.Metadata.OfficialName}}.
// {{if .Metadata.DocURL}}
// This DCP is documented in detail at: {{.Metadata.DocURL}}{{end}}
//
// Typically, use one of the New* functions to discover services on the local
// network.
2014-06-07 19:29:11 +00:00
package { { $ name } }
2014-06-07 19:07:54 +00:00
2014-06-07 19:29:41 +00:00
// Generated file - do not edit by hand. See README.md
2014-06-07 19:07:54 +00:00
import (
"time"
"github.com/huin/goupnp"
"github.com/huin/goupnp/soap"
)
// Hack to avoid Go complaining if time isn't used.
var _ time . Time
// Device URNs:
const ( { { range . DeviceTypes } }
{ { . Const } } = "{{.URN}}" { { end } }
)
// Service URNs:
const ( { { range . ServiceTypes } }
{ { . Const } } = "{{.URN}}" { { end } }
)
{ { range . Services } }
{ { $ srv := . } }
{ { $ srvIdent := printf "%s%s" . Name . Version } }
// {{$srvIdent}} is a client for UPnP SOAP service with URN "{{.URN}}". See
// goupnp.ServiceClient, which contains RootDevice and Service attributes which
// are provided for informational value.
type { { $ srvIdent } } struct {
goupnp . ServiceClient
}
// New{{$srvIdent}}Clients discovers instances of the service on the network,
// and returns clients to any that are found. errors will contain an error for
// any devices that replied but which could not be queried, and err will be set
// if the discovery process failed outright.
//
// This is a typical entry calling point into this package.
func New { { $ srvIdent } } Clients ( ) ( clients [ ] * { { $ srvIdent } } , errors [ ] error , err error ) {
var genericClients [ ] goupnp . ServiceClient
if genericClients , errors , err = goupnp . NewServiceClients ( { { $ srv . Const } } ) ; err != nil {
return
}
clients = make ( [ ] * { { $ srvIdent } } , len ( genericClients ) )
for i := range genericClients {
clients [ i ] = & { { $ srvIdent } } { genericClients [ i ] }
}
return
}
{ { range . SCPD . Actions } } { { /* loops over *SCPDWithURN values */ } }
{ { $ inargs := . InputArguments } } { { $ outargs := . OutputArguments } }
// {{if $inargs}}Arguments:{{range $inargs}}{{$argWrap := $srv.WrapArgument .}}
//
// * {{.Name}}: {{$argWrap.Document}}{{end}}{{end}}
//
// {{if $outargs}}Return values:{{range $outargs}}{{$argWrap := $srv.WrapArgument .}}
//
// * {{.Name}}: {{$argWrap.Document}}{{end}}{{end}}
func ( client * { { $ srvIdent } } ) { { . Name } } ( { { range $ inargs } } { { / *
* / } } { { $ argWrap := $ srv . WrapArgument . } } { { $ argWrap . AsParameter } } , { { end } } { { / *
* / } } ) ( { { range $ outargs } } { { / *
* / } } { { $ argWrap := $ srv . WrapArgument . } } { { $ argWrap . AsParameter } } , { { end } } err error ) {
// Request structure.
request := { { if $ inargs } } & { { template "argstruct" $ inargs } } { { "{}" } } { { else } } { { "interface{}(nil)" } } { { end } }
// BEGIN Marshal arguments into request.
{ { range $ inargs } } { { $ argWrap := $ srv . WrapArgument . } }
if request . { { . Name } } , err = { { $ argWrap . Marshal } } ; err != nil {
return
} { { end } }
// END Marshal arguments into request.
// Response structure.
response := { { if $ outargs } } & { { template "argstruct" $ outargs } } { { "{}" } } { { else } } { { "interface{}(nil)" } } { { end } }
// Perform the SOAP call.
if err = client . SOAPClient . PerformAction ( { { $ srv . URNParts . Const } } , "{{.Name}}" , request , response ) ; err != nil {
return
}
// BEGIN Unmarshal arguments from response.
{ { range $ outargs } } { { $ argWrap := $ srv . WrapArgument . } }
if { { . Name } } , err = { { $ argWrap . Unmarshal "response" } } ; err != nil {
return
} { { end } }
// END Unmarshal arguments from response.
return
}
{ { end } } { { /* range .SCPD.Actions */ } }
{ { end } } { { /* range .Services */ } }
{ { define "argstruct" } } struct { { "{" } } { { range . } }
{ { . Name } } string
{ { end } } { { "}" } } { { end } }
` ) )