Update specgen to download DCP XML specs and process them individually.

This commit is contained in:
John Beisley 2015-06-07 09:01:20 +01:00
parent 4d40860a5b
commit 6cbb8011f8
3 changed files with 110 additions and 149 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/gotasks/specs

View File

@ -15,16 +15,14 @@ Regenerating dcps generated source code:
1. Install gotasks: `go get -u github.com/jingweno/gotask` 1. Install gotasks: `go get -u github.com/jingweno/gotask`
2. Change to the gotasks directory: `cd gotasks` 2. Change to the gotasks directory: `cd gotasks`
3. Download UPnP specification data (if not done already): `wget http://upnp.org/resources/upnpresources.zip` 3. Run specgen task: `gotask specgen`
4. Regenerate source code: `gotask specgen -s upnpresources.zip -o ../dcps`
Supporting additional UPnP devices and services: Supporting additional UPnP devices and services:
------------------------------------------------ ------------------------------------------------
Supporting additional services is, in the trivial case, simply a matter of Supporting additional services is, in the trivial case, simply a matter of
adding the service to the `dcpMetadataByDir` whitelist in adding the service to the `dcpMetadata` whitelist in `gotasks/specgen_task.go`,
`gotasks/specgen_task.go`, regenerating the source code (see above), and regenerating the source code (see above), and committing that source code.
committing that source code.
However, it would be helpful if anyone needing such a service could test the However, it would be helpful if anyone needing such a service could test the
service against the service they have, and then reporting any trouble service against the service they have, and then reporting any trouble

View File

@ -4,12 +4,11 @@ package gotasks
import ( import (
"archive/zip" "archive/zip"
"bytes"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"log" "log"
"net/http"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
@ -28,6 +27,35 @@ var (
serviceURNPrefix = "urn:schemas-upnp-org:service:" serviceURNPrefix = "urn:schemas-upnp-org:service:"
) )
// DCP contains extra metadata to use when generating DCP source files.
type DCPMetadata struct {
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.
XMLSpecURL string // Where to download the XML spec from.
}
var dcpMetadata = []DCPMetadata{
{
Name: "internetgateway1",
OfficialName: "Internet Gateway Device v1",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf",
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-TestFiles-20010921.zip",
},
{
Name: "internetgateway2",
OfficialName: "Internet Gateway Device v2",
DocURL: "http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v2-Device.pdf",
XMLSpecURL: "http://upnp.org/specs/gw/UPnP-gw-IGD-Testfiles-20110224.zip",
},
{
Name: "av1",
OfficialName: "MediaServer v1 and MediaRenderer v1",
DocURL: "http://upnp.org/specs/av/av1/",
XMLSpecURL: "http://upnp.org/specs/av/UPnP-av-TestFiles-20070927.zip",
},
}
// NAME // NAME
// specgen - generates Go code from the UPnP specification files. // specgen - generates Go code from the UPnP specification files.
// //
@ -35,109 +63,80 @@ var (
// The specification is available for download from: // The specification is available for download from:
// //
// OPTIONS // OPTIONS
// -s, --spec_filename=<upnpresources.zip> // -s, --specs_dir=<spec directory>
// Path to the specification file, available from http://upnp.org/resources/upnpresources.zip // Path to the specification storage directory. This is used to find (and download if not present) the specification ZIP files. Defaults to 'specs'
// -o, --out_dir=<output directory> // -o, --out_dir=<output directory>
// 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 // 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. Defaults to '../dcps'
// --nogofmt // --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. // 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) { func TaskSpecgen(t *tasking.T) {
specFilename := t.Flags.String("spec-filename") specsDir := fallbackStrValue("specs", t.Flags.String("specs_dir"), t.Flags.String("s"))
if specFilename == "" { if err := os.MkdirAll(specsDir, os.ModePerm); err != nil {
specFilename = t.Flags.String("s") t.Fatalf("Could not create specs-dir %q: %v\n", specsDir, err)
}
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")
} }
outDir := fallbackStrValue("../dcps", t.Flags.String("out_dir"), t.Flags.String("o"))
useGofmt := !t.Flags.Bool("nogofmt") useGofmt := !t.Flags.Bool("nogofmt")
specArchive, err := openZipfile(specFilename) for _, d := range dcpMetadata {
specFilename := filepath.Join(specsDir, d.Name+".zip")
err := acquireFile(specFilename, d.XMLSpecURL)
if err != nil { if err != nil {
t.Fatalf("Error opening spec file: %v", err) t.Logf("Could not acquire spec for %s, skipping: %v\n", d.Name, err)
} }
defer specArchive.Close() dcp := newDCP(d)
if err := dcp.processZipFile(specFilename); err != nil {
dcpCol := newDcpsCollection() log.Printf("Error processing spec for %s in file %q: %v", d.Name, specFilename, err)
for _, f := range globFiles("standardizeddcps/*/*.zip", specArchive.Reader) {
dirName := strings.TrimPrefix(f.Name, "standardizeddcps/")
slashIndex := strings.Index(dirName, "/")
if slashIndex == -1 {
// Should not happen.
t.Logf("Could not find / in %q", dirName)
return
} }
dirName = dirName[:slashIndex] dcp.writePackage(outDir, useGofmt)
dcp := dcpCol.dcpForDir(dirName)
if dcp == nil {
t.Logf("No alias defined for directory %q: skipping %s\n", dirName, f.Name)
continue
} else {
t.Logf("Alias found for directory %q: processing %s\n", dirName, f.Name)
}
dcp.processZipFile(f)
}
for _, dcp := range dcpCol.dcpByAlias {
if err := dcp.writePackage(outDir, useGofmt); err != nil { if err := dcp.writePackage(outDir, useGofmt); err != nil {
log.Printf("Error writing package %q: %v", dcp.Metadata.Name, err) log.Printf("Error writing package %q: %v", dcp.Metadata.Name, err)
} }
} }
} }
// DCP contains extra metadata to use when generating DCP source files. func fallbackStrValue(defaultValue string, values ...string) string {
type DCPMetadata struct { for _, v := range values {
Name string // What to name the Go DCP package. if v != "" {
OfficialName string // Official name for the DCP. return v
DocURL string // Optional - URL for futher documentation about the DCP.
}
var dcpMetadataByDir = map[string]DCPMetadata{
"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",
},
"MediaServer_1 and MediaRenderer_1": {
Name: "av1",
OfficialName: "MediaServer v1 and MediaRenderer v1",
DocURL: "http://upnp.org/specs/av/av1/",
},
}
type dcpCollection struct {
dcpByAlias map[string]*DCP
}
func newDcpsCollection() *dcpCollection {
c := &dcpCollection{
dcpByAlias: make(map[string]*DCP),
} }
for _, metadata := range dcpMetadataByDir {
c.dcpByAlias[metadata.Name] = newDCP(metadata)
} }
return c return defaultValue
} }
func (c *dcpCollection) dcpForDir(dirName string) *DCP { func acquireFile(specFilename string, xmlSpecURL string) error {
metadata, ok := dcpMetadataByDir[dirName] if f, err := os.Open(specFilename); err != nil {
if !ok { if !os.IsNotExist(err) {
return err
}
} else {
f.Close()
return nil return nil
} }
return c.dcpByAlias[metadata.Name]
resp, err := http.Get(xmlSpecURL)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not download spec %q from %q: ",
specFilename, xmlSpecURL, resp.Status)
}
tmpFilename := specFilename + ".download"
w, err := os.Create(tmpFilename)
if err != nil {
return err
}
defer w.Close()
_, err = io.Copy(w, resp.Body)
if err != nil {
return err
}
return os.Rename(tmpFilename, specFilename)
} }
// DCP collects together information about a UPnP Device Control Protocol. // DCP collects together information about a UPnP Device Control Protocol.
@ -156,33 +155,37 @@ func newDCP(metadata DCPMetadata) *DCP {
} }
} }
func (dcp *DCP) processZipFile(file *zip.File) { func (dcp *DCP) processZipFile(filename string) error {
archive, err := openChildZip(file) archive, err := zip.OpenReader(filename)
if err != nil { if err != nil {
log.Println("Error reading child zip file:", err) return fmt.Errorf("error reading zip file %q: %v", filename, err)
return
} }
defer archive.Close()
for _, deviceFile := range globFiles("*/device/*.xml", archive) { for _, deviceFile := range globFiles("*/device/*.xml", archive) {
dcp.processDeviceFile(deviceFile) if err := dcp.processDeviceFile(deviceFile); err != nil {
return err
}
} }
for _, scpdFile := range globFiles("*/service/*.xml", archive) { for _, scpdFile := range globFiles("*/service/*.xml", archive) {
dcp.processSCPDFile(scpdFile) if err := dcp.processSCPDFile(scpdFile); err != nil {
return err
} }
}
return nil
} }
func (dcp *DCP) processDeviceFile(file *zip.File) { func (dcp *DCP) processDeviceFile(file *zip.File) error {
var device goupnp.Device var device goupnp.Device
if err := unmarshalXmlFile(file, &device); err != nil { if err := unmarshalXmlFile(file, &device); err != nil {
log.Printf("Error decoding device XML from file %q: %v", file.Name, err) return fmt.Errorf("error decoding device XML from file %q: %v", file.Name, err)
return
} }
var mainErr error
device.VisitDevices(func(d *goupnp.Device) { device.VisitDevices(func(d *goupnp.Device) {
t := strings.TrimSpace(d.DeviceType) t := strings.TrimSpace(d.DeviceType)
if t != "" { if t != "" {
u, err := extractURNParts(t, deviceURNPrefix) u, err := extractURNParts(t, deviceURNPrefix)
if err != nil { if err != nil {
log.Println(err) mainErr = err
return
} }
dcp.DeviceTypes[t] = u dcp.DeviceTypes[t] = u
} }
@ -190,11 +193,11 @@ func (dcp *DCP) processDeviceFile(file *zip.File) {
device.VisitServices(func(s *goupnp.Service) { device.VisitServices(func(s *goupnp.Service) {
u, err := extractURNParts(s.ServiceType, serviceURNPrefix) u, err := extractURNParts(s.ServiceType, serviceURNPrefix)
if err != nil { if err != nil {
log.Println(err) mainErr = err
return
} }
dcp.ServiceTypes[s.ServiceType] = u dcp.ServiceTypes[s.ServiceType] = u
}) })
return mainErr
} }
func (dcp *DCP) writePackage(outDir string, useGofmt bool) error { func (dcp *DCP) writePackage(outDir string, useGofmt bool) error {
@ -222,22 +225,21 @@ func (dcp *DCP) writePackage(outDir string, useGofmt bool) error {
return output.Close() return output.Close()
} }
func (dcp *DCP) processSCPDFile(file *zip.File) { func (dcp *DCP) processSCPDFile(file *zip.File) error {
scpd := new(scpd.SCPD) scpd := new(scpd.SCPD)
if err := unmarshalXmlFile(file, scpd); err != nil { if err := unmarshalXmlFile(file, scpd); err != nil {
log.Printf("Error decoding SCPD XML from file %q: %v", file.Name, err) return fmt.Errorf("error decoding SCPD XML from file %q: %v", file.Name, err)
return
} }
scpd.Clean() scpd.Clean()
urnParts, err := urnPartsFromSCPDFilename(file.Name) urnParts, err := urnPartsFromSCPDFilename(file.Name)
if err != nil { if err != nil {
log.Printf("Could not recognize SCPD filename %q: %v", file.Name, err) return fmt.Errorf("could not recognize SCPD filename %q: %v", file.Name, err)
return
} }
dcp.Services = append(dcp.Services, SCPDWithURN{ dcp.Services = append(dcp.Services, SCPDWithURN{
URNParts: urnParts, URNParts: urnParts,
SCPD: scpd, SCPD: scpd,
}) })
return nil
} }
type SCPDWithURN struct { type SCPDWithURN struct {
@ -333,47 +335,7 @@ var typeConvs = map[string]conv{
"uri": conv{"URI", "*url.URL"}, "uri": conv{"URI", "*url.URL"},
} }
type closeableZipReader struct { func globFiles(pattern string, archive *zip.ReadCloser) []*zip.File {
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 var files []*zip.File
for _, f := range archive.File { for _, f := range archive.File {
if matched, err := path.Match(pattern, f.Name); err != nil { if matched, err := path.Match(pattern, f.Name); err != nil {