wurfl 1.1.2 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +167 -153
- data/VERSION +1 -1
- data/lib/wurfl/command/comparator.rb +15 -32
- data/lib/wurfl/command/uamatch.rb +82 -0
- data/lib/wurfl/handset.rb +3 -18
- data/lib/wurfl/user_agent_matcher.rb +142 -0
- data/test/handset_test.rb +26 -9
- data/test/user_agent_matcher_test.rb +34 -0
- data/wurfl.gemspec +7 -4
- metadata +6 -3
- data/lib/wurfl/command/uaprofwurflcomparator.rb +0 -224
data/README.rdoc
CHANGED
@@ -1,153 +1,167 @@
|
|
1
|
-
= WURFL
|
2
|
-
|
3
|
-
WURFL is a collection of libraries and command line tools for using and manipulating the WURFL. To start using it, simply install the gem:
|
4
|
-
|
5
|
-
sudo gem install wurfl
|
6
|
-
|
7
|
-
== Tools
|
8
|
-
|
9
|
-
All tools are run via wurfltools.rb
|
10
|
-
|
11
|
-
=== loader
|
12
|
-
|
13
|
-
Is used to parse and load a WURFL XML file into memory or save a
|
14
|
-
PStore database that is used by most of the other tools. This
|
15
|
-
application creates WURFL PStore databases that are essential for use
|
16
|
-
with the other Ruby applications.
|
17
|
-
|
18
|
-
=== inspector
|
19
|
-
|
20
|
-
Is a tool that will let you do various searches and queries on the
|
21
|
-
WURFL. This is a very simple, yet powerful program for finding
|
22
|
-
information about the handsets in the WURFL. See the wurflinspector
|
23
|
-
examples section for usage.
|
24
|
-
|
25
|
-
==== Usage
|
26
|
-
|
27
|
-
The command below will search through all handsets and return the ids
|
28
|
-
of handsets that match the passed Ruby boolean evaluation
|
29
|
-
|
30
|
-
This command will return all handsets that have more than 2 colors.
|
31
|
-
|
32
|
-
wurfltools.rb inspector -d pstorehandsets.db -s '{ |hand| hand["colors"].to_i > 2 }'"
|
33
|
-
|
34
|
-
The Ruby query must go in between the single quote marks and needs to
|
35
|
-
declare the WurflHandset instance variable name.
|
36
|
-
|
37
|
-
This command shows you how you can cheat with the current design of
|
38
|
-
the wurflinspector and print more user friendly results. This example
|
39
|
-
assumes you have the command line programs sort and uniq, but that is
|
40
|
-
only to make the output look better. This example does the same as the
|
41
|
-
above, except that it prints out the brand name and model name of the
|
42
|
-
matching handsets instead of the WURFL id.
|
43
|
-
|
44
|
-
Note: this should all go into one command line call
|
45
|
-
|
46
|
-
wurfltools.rb inspector -d pstorehandsets.db -s '{|hand| puts
|
47
|
-
"#{hand["brand_name"]} #{hand["model_name"]}" if hand["colors"].to_i >
|
48
|
-
2}' | sort | uniq"
|
49
|
-
|
50
|
-
|
51
|
-
The following individual handset query commands will tell the value of
|
52
|
-
capabilities and from where it obtained the setting.
|
53
|
-
|
54
|
-
A command to query the handset with the id sonyericsson_t300_ver1
|
55
|
-
for all of its' capabilities:
|
56
|
-
|
57
|
-
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1
|
58
|
-
|
59
|
-
A command to query the handset with the id sonyericsson_t300_ver1 for
|
60
|
-
backlight capability:
|
61
|
-
|
62
|
-
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1 -q
|
63
|
-
backlight
|
64
|
-
|
65
|
-
===
|
66
|
-
|
67
|
-
Is a
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
The
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
the
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
1
|
+
= WURFL
|
2
|
+
|
3
|
+
WURFL is a collection of libraries and command line tools for using and manipulating the WURFL. To start using it, simply install the gem:
|
4
|
+
|
5
|
+
sudo gem install wurfl
|
6
|
+
|
7
|
+
== Tools
|
8
|
+
|
9
|
+
All tools are run via wurfltools.rb
|
10
|
+
|
11
|
+
=== loader
|
12
|
+
|
13
|
+
Is used to parse and load a WURFL XML file into memory or save a
|
14
|
+
PStore database that is used by most of the other tools. This
|
15
|
+
application creates WURFL PStore databases that are essential for use
|
16
|
+
with the other Ruby applications.
|
17
|
+
|
18
|
+
=== inspector
|
19
|
+
|
20
|
+
Is a tool that will let you do various searches and queries on the
|
21
|
+
WURFL. This is a very simple, yet powerful program for finding
|
22
|
+
information about the handsets in the WURFL. See the wurflinspector
|
23
|
+
examples section for usage.
|
24
|
+
|
25
|
+
==== Usage
|
26
|
+
|
27
|
+
The command below will search through all handsets and return the ids
|
28
|
+
of handsets that match the passed Ruby boolean evaluation
|
29
|
+
|
30
|
+
This command will return all handsets that have more than 2 colors.
|
31
|
+
|
32
|
+
wurfltools.rb inspector -d pstorehandsets.db -s '{ |hand| hand["colors"].to_i > 2 }'"
|
33
|
+
|
34
|
+
The Ruby query must go in between the single quote marks and needs to
|
35
|
+
declare the WurflHandset instance variable name.
|
36
|
+
|
37
|
+
This command shows you how you can cheat with the current design of
|
38
|
+
the wurflinspector and print more user friendly results. This example
|
39
|
+
assumes you have the command line programs sort and uniq, but that is
|
40
|
+
only to make the output look better. This example does the same as the
|
41
|
+
above, except that it prints out the brand name and model name of the
|
42
|
+
matching handsets instead of the WURFL id.
|
43
|
+
|
44
|
+
Note: this should all go into one command line call
|
45
|
+
|
46
|
+
wurfltools.rb inspector -d pstorehandsets.db -s '{|hand| puts
|
47
|
+
"#{hand["brand_name"]} #{hand["model_name"]}" if hand["colors"].to_i >
|
48
|
+
2}' | sort | uniq"
|
49
|
+
|
50
|
+
|
51
|
+
The following individual handset query commands will tell the value of
|
52
|
+
capabilities and from where it obtained the setting.
|
53
|
+
|
54
|
+
A command to query the handset with the id sonyericsson_t300_ver1
|
55
|
+
for all of its' capabilities:
|
56
|
+
|
57
|
+
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1
|
58
|
+
|
59
|
+
A command to query the handset with the id sonyericsson_t300_ver1 for
|
60
|
+
backlight capability:
|
61
|
+
|
62
|
+
wurfltools.rb inspector -d pstorehandsets.db -i sonyericsson_t300_ver1 -q
|
63
|
+
backlight
|
64
|
+
|
65
|
+
=== uamatch
|
66
|
+
|
67
|
+
Is a tool that will let you do various searches and queries on the
|
68
|
+
WURFL based on user-agent strings. This is a very simple, yet powerful
|
69
|
+
program for finding information about the handsets in the WURFL.
|
70
|
+
See the uamatch examples section for usage.
|
71
|
+
|
72
|
+
The uamatcher will retrieve the handset descriptions with the closest
|
73
|
+
Levenshtein distance (also called the edit distance) to the user-agent
|
74
|
+
string you pass as argument.
|
75
|
+
|
76
|
+
For details see: http://en.wikipedia.org/wiki/Levenshtein_distance
|
77
|
+
|
78
|
+
When more than one handset has the same distance to the user agent string queried,
|
79
|
+
the command will iterate over the result set and print the result for each handset.
|
80
|
+
|
81
|
+
The found wurfl_ids can serve as input to the inspector command.
|
82
|
+
You can also specifically query for a certain capability.
|
83
|
+
|
84
|
+
==== Usage
|
85
|
+
|
86
|
+
The following individual handset query commands will tell the value of
|
87
|
+
capabilities and from where it obtained the setting.
|
88
|
+
|
89
|
+
A command to query the handset with the user-agent SL55 for all of its capabilities:
|
90
|
+
|
91
|
+
wurfltools.rb uamatch -d pstorehandsets.db -u SL55
|
92
|
+
|
93
|
+
A command to query the handset with the id SL55 for the wallpaper_jpg capability:
|
94
|
+
|
95
|
+
wurfltools.rb uamatch -d pstorehandsets.db -u SL55 -q wallpaper_jpg
|
96
|
+
|
97
|
+
=== sanitycheck
|
98
|
+
|
99
|
+
Is a partial WURFL validating program. It does a few simple
|
100
|
+
checks to make sure the XML structure is parse-able by the wurflloader.
|
101
|
+
If you receive loading errors by the wurflloader, then you can run the
|
102
|
+
wurflsanitycheck program to find the lines in the XML file that might
|
103
|
+
be causing the problem.
|
104
|
+
|
105
|
+
=== comparator
|
106
|
+
|
107
|
+
Is a another simple program that will find the differences from two
|
108
|
+
WURFL Ruby PStore databases. This is another way of finding changes
|
109
|
+
from the different versions of the WURFL without running a diff on the
|
110
|
+
XML files.
|
111
|
+
|
112
|
+
=== uaproftowurfl
|
113
|
+
|
114
|
+
Is a program that takes UAProfiles and creates an equivalent WURFL
|
115
|
+
entry. It holds all of the mappings used to convert a UAProfile to a
|
116
|
+
WURFL entry. This program alone makes using the Ruby tools worth it.
|
117
|
+
|
118
|
+
The mappings are not fully complete and can certainly use your input
|
119
|
+
in improving them.
|
120
|
+
|
121
|
+
About the source code:
|
122
|
+
The main piece of code to read are the methods under the
|
123
|
+
"UAProfile Mappings" comment.
|
124
|
+
|
125
|
+
For now you can ignore the details above this comment.
|
126
|
+
Basically each method is the name of a UAProfifle component.
|
127
|
+
When you parse a UAProfile file it will call each UAProfile
|
128
|
+
component's method name.
|
129
|
+
|
130
|
+
So you can simply look at UAProfile component's method to see
|
131
|
+
how it maps to the WURFL.
|
132
|
+
If a component is not found as a method it will be logged to
|
133
|
+
Standard Error. For this log one can then add the method to
|
134
|
+
the UAProfToWurfl class later.
|
135
|
+
|
136
|
+
==== Usage
|
137
|
+
|
138
|
+
A simple usage is:
|
139
|
+
|
140
|
+
wurfltools.rb uaproftowurfl sampleprofile.xml
|
141
|
+
|
142
|
+
Example use from a bash shell with many profiles:
|
143
|
+
|
144
|
+
for i in `ls profiles`; do
|
145
|
+
wurfltools.rb uaproftowurfl profiles/$i >output/$i.wurfl
|
146
|
+
2> output/$i.parser.err;
|
147
|
+
done
|
148
|
+
|
149
|
+
This assumes that you have a profiles directory with all of the UAProf
|
150
|
+
file you wish to parse and a output directory to place the results and
|
151
|
+
errors.
|
152
|
+
|
153
|
+
== Authors
|
154
|
+
|
155
|
+
* Zev Blut (Original Author)
|
156
|
+
* Paul McMahon (gem installer, refactorings)
|
157
|
+
* Kai W. Zimmermann (uamatch)
|
158
|
+
|
159
|
+
== Copyright
|
160
|
+
|
161
|
+
Copyright (c) 2009, mobalean (http://www.mobalean.com/)
|
162
|
+
|
163
|
+
Copyright (c) 2003, Ubiquitous Business Technology (http://ubit.com)
|
164
|
+
All rights reserved.
|
165
|
+
|
166
|
+
Send enquiries to: mailto:info@mobalean.com
|
167
|
+
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.2.0
|
@@ -13,10 +13,11 @@ class Wurfl::Command::Comparator < Wurfl::Command
|
|
13
13
|
|
14
14
|
def display_differences(hand1, hand2)
|
15
15
|
puts "-------------------------------------"
|
16
|
-
puts "
|
17
|
-
hand1.
|
16
|
+
puts "WURFL_ID: #{hand1.wurfl_id}"
|
17
|
+
puts "Handset 1: #{hand1.user_agent}"
|
18
|
+
puts "Handset 2: #{hand2.user_agent}"
|
19
|
+
hand1.differences(hand2).each do |key|
|
18
20
|
v1, v2 = hand1[key], hand2[key]
|
19
|
-
next if v1.nil? || v2.nil? || v1 == v2
|
20
21
|
puts "Key:#{key}"
|
21
22
|
puts "h1>:#{hand1[key]}"
|
22
23
|
puts "h2<:#{hand2[key]}"
|
@@ -49,40 +50,22 @@ class Wurfl::Command::Comparator < Wurfl::Command
|
|
49
50
|
puts "Comparing files: #{ARGV[0]} and #{ARGV[1]}"
|
50
51
|
puts "-------------------------------------"
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
different = Array.new
|
62
|
-
mwurfl.each do |key,handset|
|
63
|
-
if lwurfl.key?(key)
|
64
|
-
if handset != lwurfl[key]
|
65
|
-
different << [handset,lwurfl[key]]
|
66
|
-
display_differences(handset,lwurfl[key])
|
67
|
-
end
|
68
|
-
else
|
69
|
-
notfound<< handset
|
53
|
+
wurfl1_unknown, wurfl2_unknown, different = [],[],[]
|
54
|
+
(wurfl1.keys | wurfl2.keys).each do |key|
|
55
|
+
handset1, handset2 = wurfl1[key], wurfl2[key]
|
56
|
+
if !handset1
|
57
|
+
wurfl1_unknown << key
|
58
|
+
elsif !handset2
|
59
|
+
wurfl2_unknown << key
|
60
|
+
elsif handset1 != handset2
|
61
|
+
display_differences(handset1,handset2)
|
70
62
|
end
|
71
63
|
end
|
72
64
|
|
73
65
|
|
74
66
|
puts "Comparision complete."
|
75
67
|
|
76
|
-
puts "
|
77
|
-
puts "
|
78
|
-
notfound = notfound.sort { |x,y| y.wurfl_id <=> x.wurfl_id }
|
79
|
-
notfound.each { |hand| puts hand.wurfl_id }
|
80
|
-
puts "||||||||||||||||||||||||||||||||||||"
|
81
|
-
|
82
|
-
puts "Different handsets: #{different.size}"
|
83
|
-
puts "||||||||||||||||||||||||||||||||||||"
|
84
|
-
different = different.sort { |x,y| y.first.wurfl_id <=> x.first.wurfl_id }
|
85
|
-
|
86
|
-
puts "||||||||||||||||||||||||||||||||||||"
|
68
|
+
puts "Handsets not found in wurfl1: #{wurfl1_unknown.inspect}"
|
69
|
+
puts "Handsets not found in wurfl2: #{wurfl2_unknown.inspect}"
|
87
70
|
end
|
88
71
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require "wurfl/command"
|
2
|
+
require "wurfl/utils"
|
3
|
+
require "wurfl/handset"
|
4
|
+
require "wurfl/user_agent_matcher"
|
5
|
+
require "getoptlong"
|
6
|
+
|
7
|
+
=begin
|
8
|
+
The uamatch command itself is based on the inspector command found in this
|
9
|
+
library.
|
10
|
+
|
11
|
+
Author: Kai W. Zimmermann (kwz@kai-zimmermann.de)
|
12
|
+
=end
|
13
|
+
class Wurfl::Command::Uamatch < Wurfl::Command
|
14
|
+
include Wurfl::Utils
|
15
|
+
|
16
|
+
def usage
|
17
|
+
puts "Usage: wurfltools.rb uamatch [-u user_agent [-q attributename]] -d pstorefile"
|
18
|
+
puts "Examples:"
|
19
|
+
puts "wurfltools.rb uamatch -d pstorehandsets.db -u SL55"
|
20
|
+
puts "wurfltools.rb uamatch -d pstorehandsets.db -u SL55 -q wallpaper_jpg"
|
21
|
+
exit 1
|
22
|
+
end
|
23
|
+
|
24
|
+
def execute
|
25
|
+
pstorefile = nil
|
26
|
+
useragent = nil
|
27
|
+
query = nil
|
28
|
+
begin
|
29
|
+
opt = GetoptLong.new(
|
30
|
+
["-d","--database", GetoptLong::REQUIRED_ARGUMENT],
|
31
|
+
["-u","--useragent", GetoptLong::REQUIRED_ARGUMENT],
|
32
|
+
["-q","--query", GetoptLong::REQUIRED_ARGUMENT]
|
33
|
+
)
|
34
|
+
|
35
|
+
opt.each do |arg,val|
|
36
|
+
case arg
|
37
|
+
when "-d"
|
38
|
+
pstorefile = val
|
39
|
+
when "-u"
|
40
|
+
useragent = val
|
41
|
+
when "-q"
|
42
|
+
query = val
|
43
|
+
else
|
44
|
+
usage
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
rescue => err
|
49
|
+
usage
|
50
|
+
end
|
51
|
+
|
52
|
+
if !pstorefile
|
53
|
+
puts "You must specify a Wurfl PStore db"
|
54
|
+
usage
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
handsets, = load_wurfl_pstore(pstorefile)
|
59
|
+
puts "Number of Handsets: #{handsets.length}"
|
60
|
+
uamatch = Wurfl::UserAgentMatcher.new(handsets)
|
61
|
+
rescue => err
|
62
|
+
STDERR.puts "Error with file #{pstorefile}"
|
63
|
+
STDERR.puts err.message
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
if useragent
|
68
|
+
matches, distance = uamatch.match_handsets(useragent)
|
69
|
+
puts "Shortest distance #{distance}, #{matches.length} match#{'es' if (matches.length!=1)}"
|
70
|
+
matches.each do |handset|
|
71
|
+
puts "Handset wurfl id: #{handset.wurfl_id}"
|
72
|
+
puts "User_agent found: #{handset.user_agent}"
|
73
|
+
if query
|
74
|
+
puts "Result of handset query: #{query}"
|
75
|
+
rez = handset.get_value_and_owner(query)
|
76
|
+
puts "#{rez[0]} from #{rez[1]}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
exit 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/wurfl/handset.rb
CHANGED
@@ -86,24 +86,9 @@ class Wurfl::Handset
|
|
86
86
|
other.keys.all? {|key| other[key] == self[key] }
|
87
87
|
end
|
88
88
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
# Returns:
|
93
|
-
# An array of the different values.
|
94
|
-
# Each entry in the Array is an Array of three values.
|
95
|
-
# The first value is the key in which both handsets have different values.
|
96
|
-
# The second is the other handset's value for the key.
|
97
|
-
# The third is the handset id from where the other handset got it's value.
|
98
|
-
def compare(other)
|
99
|
-
differences = Array.new
|
100
|
-
self.keys.each do |key|
|
101
|
-
oval,oid = other.get_value_and_owner(key)
|
102
|
-
if @capabilityhash[key].to_s != oval.to_s
|
103
|
-
differences << [key,oval,oid]
|
104
|
-
end
|
105
|
-
end
|
106
|
-
differences
|
89
|
+
def differences(other)
|
90
|
+
keys = (self.keys | other.keys)
|
91
|
+
keys.find_all {|k| self[k] != other[k]}
|
107
92
|
end
|
108
93
|
|
109
94
|
class NullHandset
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Wurfl; end
|
2
|
+
|
3
|
+
=begin
|
4
|
+
A class that lists wurfl handsets that match user_agents using the shortest
|
5
|
+
Levenshtein distance, also sometimes called the edit distance,
|
6
|
+
see http://en.wikipedia.org/wiki/Levenshtein_distance
|
7
|
+
|
8
|
+
The implementation of the Levenshtein distance used here is based on the
|
9
|
+
algorithm found in the Text gem originally written by Paul Battley
|
10
|
+
(pbattley@gmail.com)
|
11
|
+
|
12
|
+
The implementation given here makes heavy use of optimizations based on
|
13
|
+
the estimation of the lower bound that can be achieved. Depending on the
|
14
|
+
length of the user agent this brought an over all increase by a factor of
|
15
|
+
about 40, although it renders the code slightly unreadable. In general the
|
16
|
+
longer the user agent string and the greater the result distance, the longer
|
17
|
+
the computation takes.
|
18
|
+
|
19
|
+
Author: Kai W. Zimmermann (kwz@kai-zimmermann.de)
|
20
|
+
=end
|
21
|
+
class Wurfl::UserAgentMatcher
|
22
|
+
# Constructor
|
23
|
+
# Parameters:
|
24
|
+
# handsets: A hashtable of wurfl handsets indexed by wurfl_id.
|
25
|
+
def initialize(handsets)
|
26
|
+
@handsets = handsets
|
27
|
+
@longest_user_agent_length = @handsets.values.inject(0) do |m,hand|
|
28
|
+
hand.user_agent.length > m ? hand.user_agent.length : m
|
29
|
+
end
|
30
|
+
@d=(0..@longest_user_agent_length).to_a
|
31
|
+
end
|
32
|
+
|
33
|
+
# A method to retrieve a list of the uamatcher's handsets that match
|
34
|
+
# the passed user_agent closest using the Levenshtein distance.
|
35
|
+
# Parameters:
|
36
|
+
# user_agent: is a user_agent string to be matched
|
37
|
+
# Returns:
|
38
|
+
# An Array of all WurflHandsets that match the user_agent closest with the
|
39
|
+
# same distance
|
40
|
+
# The Levenshtein distance for these matches
|
41
|
+
def match_handsets(user_agent)
|
42
|
+
rez = []
|
43
|
+
shortest_distance = [user_agent.length, @longest_user_agent_length].max
|
44
|
+
s = user_agent.unpack(unpack_rule)
|
45
|
+
|
46
|
+
@handsets.values.each do |hand|
|
47
|
+
distance = levenshtein_distance(user_agent, hand.user_agent, shortest_distance, s)
|
48
|
+
# found a shorter distance match, flush old results
|
49
|
+
rez.clear if shortest_distance > distance
|
50
|
+
|
51
|
+
if shortest_distance >= distance
|
52
|
+
# always add the first handset matched and each that has the same
|
53
|
+
# distance as the shortest distance so far
|
54
|
+
rez << hand
|
55
|
+
shortest_distance = distance
|
56
|
+
end
|
57
|
+
|
58
|
+
break if shortest_distance == 0
|
59
|
+
end
|
60
|
+
|
61
|
+
return rez, shortest_distance
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# A method to estimate and compute the Levenshtein distance (LD) based on the
|
67
|
+
# implementation from the Text gem. The implementation given here applies
|
68
|
+
# known upper and lower bounds as found at:
|
69
|
+
# http://en.wikipedia.org/wiki/Levenshtein_distance
|
70
|
+
# LD is always at least the difference of the sizes of the two strings.
|
71
|
+
# -> We can safely discard the handset if the least distance is longer than
|
72
|
+
# the current shortest distance
|
73
|
+
# LD is zero if and only if the strings are identical.
|
74
|
+
# -> We can optimize the test for equality and stop searching after an exact
|
75
|
+
# match
|
76
|
+
# Parameters:
|
77
|
+
# str1: is the user-agent to look up
|
78
|
+
# str2: is the user-agent to compare against
|
79
|
+
# min: is the minimum distance found so far
|
80
|
+
# s: the unpacked version of the user-agent string we look up
|
81
|
+
# Returns:
|
82
|
+
# It returns the least bound estimation if the least bound is already greater
|
83
|
+
# than the current minimum distance
|
84
|
+
# Otherwise it will compute the Levenshtein distance of str1 and str2
|
85
|
+
# It optimizes the check for equality
|
86
|
+
def levenshtein_distance(str1, str2, min, s)
|
87
|
+
diff = (str1.length - str2.length).abs
|
88
|
+
return 0 if diff == 0 && str1 == str2
|
89
|
+
return diff if diff > min
|
90
|
+
t = str2.unpack(unpack_rule)
|
91
|
+
distance(s, t, min)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Compute the Levenshtein distance or stop if the minimum found so far will
|
95
|
+
# be exceeded.
|
96
|
+
# Optimizations: Avoid GC, where possible reuse array
|
97
|
+
# The distance computation in the outer loop is monotonously decreasing thus
|
98
|
+
# we can safely stop if the estimated possible minimum exceeds the current
|
99
|
+
# minimum
|
100
|
+
# Parameters:
|
101
|
+
# s: is the first string, already unpacked
|
102
|
+
# t: is the second string, already unpacked
|
103
|
+
# min: is the minimum distance found so far
|
104
|
+
# Returns:
|
105
|
+
# The routine returns the computed distance or the minimum found so far if the # distance
|
106
|
+
# computed so far exceeds the minimum
|
107
|
+
def distance(s, t, min)
|
108
|
+
n = s.length
|
109
|
+
m = t.length
|
110
|
+
return m if 0 == n
|
111
|
+
return n if 0 == m
|
112
|
+
|
113
|
+
d = @d # Optimization: Avoid GC by reusing array
|
114
|
+
(0...m).each do |j|
|
115
|
+
d[j] = j
|
116
|
+
end
|
117
|
+
|
118
|
+
x = 0
|
119
|
+
(0...n).each do |i|
|
120
|
+
e = i+1
|
121
|
+
(0...m).each do |j|
|
122
|
+
cost = (s[i] == t[j]) ? 0 : 1
|
123
|
+
x = d[j+1] + 1 # insertion
|
124
|
+
y = e+1
|
125
|
+
x = y if y < x # deletion
|
126
|
+
z = d[j] + cost
|
127
|
+
x = z if z < x # substitution
|
128
|
+
d[j] = e
|
129
|
+
e = x
|
130
|
+
end
|
131
|
+
d[m] = x
|
132
|
+
# estimate the minimum LD that still can be achieved, this will be
|
133
|
+
# increasing monotonously stop once we exceed the current minimum
|
134
|
+
return x - n + i + 1 if x - n + i + 1 > min
|
135
|
+
end
|
136
|
+
x
|
137
|
+
end
|
138
|
+
|
139
|
+
def unpack_rule
|
140
|
+
$KCODE =~ /^U/i ? 'U*' : 'C*'
|
141
|
+
end
|
142
|
+
end
|
data/test/handset_test.rb
CHANGED
@@ -6,6 +6,9 @@ class TestHandset < Test::Unit::TestCase
|
|
6
6
|
def setup
|
7
7
|
@f = Wurfl::Handset.new("f", "f", nil)
|
8
8
|
@h = Wurfl::Handset.new("h", "h", @f)
|
9
|
+
|
10
|
+
@f2 = Wurfl::Handset.new("f2", "f2_ua", nil)
|
11
|
+
@h2 = Wurfl::Handset.new("h2","h2_ua", @f2)
|
9
12
|
end
|
10
13
|
|
11
14
|
def test_f
|
@@ -51,20 +54,34 @@ class TestHandset < Test::Unit::TestCase
|
|
51
54
|
assert @h == h2
|
52
55
|
end
|
53
56
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
57
|
+
def test_differences_handset_with_unmodified_fallback
|
58
|
+
assert @h.differences(@f).empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_differences_handset_with_identical_handset
|
62
|
+
assert @h.differences(@h2).empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_differences_handset_that_has_extra_key
|
58
66
|
@h["k"] = "v"
|
67
|
+
assert_equal ["k"], @h.differences(@h2)
|
68
|
+
end
|
59
69
|
|
60
|
-
|
70
|
+
def test_differences_other_handset_that_has_extra_key
|
71
|
+
@h2["k"] = "v"
|
72
|
+
assert_equal ["k"], @h.differences(@h2)
|
73
|
+
end
|
61
74
|
|
62
|
-
|
63
|
-
|
75
|
+
def test_differences_handset_that_has_differing_key_value
|
76
|
+
@h["k"] = "v"
|
77
|
+
@h2["k"] = "v2"
|
78
|
+
assert_equal ["k"], @h.differences(@h2)
|
79
|
+
end
|
64
80
|
|
81
|
+
def test_differences_handsets_with_differening_fallback_key_value
|
65
82
|
@f["j"] = "1"
|
66
|
-
f2["j"] = "2"
|
67
|
-
assert_equal [
|
83
|
+
@f2["j"] = "2"
|
84
|
+
assert_equal ["j"], @h.differences(@h2)
|
68
85
|
end
|
69
86
|
|
70
87
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
2
|
+
require 'wurfl/user_agent_matcher'
|
3
|
+
require 'wurfl/loader'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
class UserAgentMatcherTest < Test::Unit::TestCase
|
7
|
+
def setup
|
8
|
+
loader = Wurfl::Loader.new
|
9
|
+
handsets, fallbacks = loader.load_wurfl(File.join(File.dirname(__FILE__), "data", "wurfl.simple.xml"))
|
10
|
+
@matcher = Wurfl::UserAgentMatcher.new(handsets)
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_empty_user_agent
|
14
|
+
a, shortest_distance = @matcher.match_handsets("")
|
15
|
+
assert_equal 1, a.size
|
16
|
+
assert_equal "generic_xhtml", a.first.wurfl_id
|
17
|
+
assert_equal 4, shortest_distance
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_matching_user_agent
|
21
|
+
a, shortest_distance = @matcher.match_handsets("generic")
|
22
|
+
assert_equal 1, a.size
|
23
|
+
assert_equal "generic", a.first.wurfl_id
|
24
|
+
assert_equal 0, shortest_distance
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_iphone
|
28
|
+
s = "Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en)"
|
29
|
+
a, shortest_distance = @matcher.match_handsets(s)
|
30
|
+
assert_equal 1, a.size
|
31
|
+
assert_equal "apple_generic", a.first.wurfl_id
|
32
|
+
assert_equal 26, shortest_distance
|
33
|
+
end
|
34
|
+
end
|
data/wurfl.gemspec
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = %q{wurfl}
|
5
|
-
s.version = "1.
|
5
|
+
s.version = "1.2.0"
|
6
6
|
|
7
7
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
8
|
s.authors = ["Paul McMahon", "Zev Blut"]
|
9
|
-
s.date = %q{2009-07-
|
9
|
+
s.date = %q{2009-07-22}
|
10
10
|
s.default_executable = %q{wurfltools.rb}
|
11
11
|
s.description = %q{TODO}
|
12
12
|
s.email = %q{info@mobalean.com}
|
@@ -27,15 +27,17 @@ Gem::Specification.new do |s|
|
|
27
27
|
"lib/wurfl/command/inspector.rb",
|
28
28
|
"lib/wurfl/command/loader.rb",
|
29
29
|
"lib/wurfl/command/sanitycheck.rb",
|
30
|
+
"lib/wurfl/command/uamatch.rb",
|
30
31
|
"lib/wurfl/command/uaproftowurfl.rb",
|
31
|
-
"lib/wurfl/command/uaprofwurflcomparator.rb",
|
32
32
|
"lib/wurfl/handset.rb",
|
33
33
|
"lib/wurfl/loader.rb",
|
34
34
|
"lib/wurfl/uaproftowurfl.rb",
|
35
|
+
"lib/wurfl/user_agent_matcher.rb",
|
35
36
|
"lib/wurfl/utils.rb",
|
36
37
|
"test/data/wurfl.simple.xml",
|
37
38
|
"test/handset_test.rb",
|
38
39
|
"test/loader_test.rb",
|
40
|
+
"test/user_agent_matcher_test.rb",
|
39
41
|
"test/utils_test.rb",
|
40
42
|
"wurfl.gemspec"
|
41
43
|
]
|
@@ -49,7 +51,8 @@ Gem::Specification.new do |s|
|
|
49
51
|
s.test_files = [
|
50
52
|
"test/loader_test.rb",
|
51
53
|
"test/handset_test.rb",
|
52
|
-
"test/utils_test.rb"
|
54
|
+
"test/utils_test.rb",
|
55
|
+
"test/user_agent_matcher_test.rb"
|
53
56
|
]
|
54
57
|
|
55
58
|
if s.respond_to? :specification_version then
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wurfl
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul McMahon
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2009-07-
|
13
|
+
date: 2009-07-22 00:00:00 +09:00
|
14
14
|
default_executable: wurfltools.rb
|
15
15
|
dependencies: []
|
16
16
|
|
@@ -35,15 +35,17 @@ files:
|
|
35
35
|
- lib/wurfl/command/inspector.rb
|
36
36
|
- lib/wurfl/command/loader.rb
|
37
37
|
- lib/wurfl/command/sanitycheck.rb
|
38
|
+
- lib/wurfl/command/uamatch.rb
|
38
39
|
- lib/wurfl/command/uaproftowurfl.rb
|
39
|
-
- lib/wurfl/command/uaprofwurflcomparator.rb
|
40
40
|
- lib/wurfl/handset.rb
|
41
41
|
- lib/wurfl/loader.rb
|
42
42
|
- lib/wurfl/uaproftowurfl.rb
|
43
|
+
- lib/wurfl/user_agent_matcher.rb
|
43
44
|
- lib/wurfl/utils.rb
|
44
45
|
- test/data/wurfl.simple.xml
|
45
46
|
- test/handset_test.rb
|
46
47
|
- test/loader_test.rb
|
48
|
+
- test/user_agent_matcher_test.rb
|
47
49
|
- test/utils_test.rb
|
48
50
|
- wurfl.gemspec
|
49
51
|
has_rdoc: true
|
@@ -76,3 +78,4 @@ test_files:
|
|
76
78
|
- test/loader_test.rb
|
77
79
|
- test/handset_test.rb
|
78
80
|
- test/utils_test.rb
|
81
|
+
- test/user_agent_matcher_test.rb
|
@@ -1,224 +0,0 @@
|
|
1
|
-
require "wurfl/command"
|
2
|
-
require "getoptlong"
|
3
|
-
require "net/http"
|
4
|
-
|
5
|
-
require "wurfl/uaproftowurfl"
|
6
|
-
require "wurfl/handset"
|
7
|
-
require "wurfl/utils"
|
8
|
-
include Wurfl::Utils
|
9
|
-
|
10
|
-
# An addition to the UAProf to Wurfl to generate a WurflHandset from the UAProf.
|
11
|
-
class Wurfl::UAProfToWURLF
|
12
|
-
def make_wurfl_handset
|
13
|
-
hand = Wurfl::Handset.new("UAProf",@wurfl["user_agent"])
|
14
|
-
@wurflgroups.each do |group|
|
15
|
-
@wurfl[group].sort.each do |key,value|
|
16
|
-
hand[key] = value
|
17
|
-
end
|
18
|
-
end
|
19
|
-
return hand
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
|
24
|
-
class Wurfl::Command::Uaprofwurflcomparator
|
25
|
-
def parse_mapping_file(file)
|
26
|
-
if !File.exist?(file)
|
27
|
-
$stderr.puts "Mapping File does not exist. File passed was #{file}."
|
28
|
-
return Array.new
|
29
|
-
end
|
30
|
-
mappings = Array.new
|
31
|
-
f = File.new(file)
|
32
|
-
f.each do |line|
|
33
|
-
if m = /^"(.*)" "(.*)"$/.match(line.strip)
|
34
|
-
uaprof = m[1]
|
35
|
-
useragent = m[2]
|
36
|
-
mappings<< [uaprof,useragent]
|
37
|
-
else
|
38
|
-
$stderr.puts "Irregular format for line: #{line}" if line.strip != ""
|
39
|
-
end
|
40
|
-
end
|
41
|
-
f.close
|
42
|
-
|
43
|
-
return mappings
|
44
|
-
end
|
45
|
-
|
46
|
-
def get_uaprofile(uaprof,profiledir,check=false)
|
47
|
-
file = strip_uaprof(uaprof)
|
48
|
-
if File.exists?("#{profiledir}/#{file}") && check
|
49
|
-
return file
|
50
|
-
end
|
51
|
-
|
52
|
-
get_and_save_uaprof_file(uaprof,profiledir)
|
53
|
-
return file
|
54
|
-
end
|
55
|
-
|
56
|
-
def strip_uaprof(uaprof)
|
57
|
-
uaprof_file = nil
|
58
|
-
if m = /([^\/]*)$/.match(uaprof)
|
59
|
-
uaprof_file = m[1]
|
60
|
-
else
|
61
|
-
$stderr.puts "Cannot find the base UAProf file in URI: #{uaprof}"
|
62
|
-
end
|
63
|
-
return uaprof_file
|
64
|
-
end
|
65
|
-
|
66
|
-
def load_pstore(pstorefile)
|
67
|
-
hands = Hash.new
|
68
|
-
begin
|
69
|
-
handsid, = load_wurfl_pstore(pstorefile)
|
70
|
-
handsid.each { |id,val| hands[val.user_agent] = val }
|
71
|
-
rescue => err
|
72
|
-
$stderr.puts "Error: Cannot load PStore file. #{pstorefile}"
|
73
|
-
$stderr.puts err.message
|
74
|
-
exit 1
|
75
|
-
end
|
76
|
-
return hands
|
77
|
-
end
|
78
|
-
|
79
|
-
def get_and_save_uaprof_file(uaprof_url,savedirectory,limit=0)
|
80
|
-
base,path,port = parse_url(uaprof_url)
|
81
|
-
|
82
|
-
raise "Too many redirects from original url" if limit > 3
|
83
|
-
raise "Unparseable URL: #{url}" if base.nil?
|
84
|
-
|
85
|
-
port = 80 if port.nil?
|
86
|
-
http = Net::HTTP.new(base,port)
|
87
|
-
begin
|
88
|
-
resp, data = http.get(path)
|
89
|
-
if resp.code == "301"
|
90
|
-
# get location and call self again
|
91
|
-
http.finish
|
92
|
-
limit += 1
|
93
|
-
get_and_save_uaprof_file(resp['location'],savedirectory,limit)
|
94
|
-
return
|
95
|
-
elsif resp.code != "200"
|
96
|
-
raise "Unexpected HTTP Response code:#{resp.code} for #{uaprof_url}"
|
97
|
-
end
|
98
|
-
rescue => err
|
99
|
-
raise
|
100
|
-
end
|
101
|
-
|
102
|
-
f = File.new("#{savedirectory}/#{strip_uaprof(path)}","w")
|
103
|
-
f.write(data)
|
104
|
-
f.close
|
105
|
-
|
106
|
-
end
|
107
|
-
|
108
|
-
def parse_url(url)
|
109
|
-
m = /(http:\/\/)?(.*?)(:(\d*))?\/(.*)/i.match(url.strip)
|
110
|
-
|
111
|
-
return nil if m.nil?
|
112
|
-
return m[2],"/#{m[5]}",m[4]
|
113
|
-
end
|
114
|
-
|
115
|
-
def usage
|
116
|
-
puts "Usage: wurfltools.rb uaprofwurflcomparator -d profiledirectory -f mappingfile [-w wurfldb] [-c] [-h | --help]"
|
117
|
-
puts "Examples:"
|
118
|
-
puts "wurfltools.rb uaprofwurflcomparator -d ./profiles -f all-profile.2003-08.log -c -w wurfl.db"
|
119
|
-
exit 1
|
120
|
-
end
|
121
|
-
|
122
|
-
def help
|
123
|
-
puts "-d --directory : The directory to store the UA Profiles found in the log file."
|
124
|
-
puts "-f --file : The log file that has a UAProfile to User-Agent mapping per line."
|
125
|
-
puts "-c --check : A flag that will make sure to check if the profile is already in the directory or not. If it is not then it will download it."
|
126
|
-
puts "-w --wurfldb : A Ruby PStore Database of the WURFL, that is used to compare against the UAProfiles."
|
127
|
-
puts "-h --help : This message."
|
128
|
-
exit 1
|
129
|
-
end
|
130
|
-
|
131
|
-
def execute
|
132
|
-
profiledirectory = mappingfile = pstorefile = nil
|
133
|
-
existancecheck = false
|
134
|
-
begin
|
135
|
-
opt = GetoptLong.new(
|
136
|
-
["-d","--directory", GetoptLong::REQUIRED_ARGUMENT],
|
137
|
-
["-f","--file", GetoptLong::REQUIRED_ARGUMENT],
|
138
|
-
["-c","--check", GetoptLong::NO_ARGUMENT],
|
139
|
-
["-h","--help", GetoptLong::NO_ARGUMENT],
|
140
|
-
["-w","--wurfldb", GetoptLong::REQUIRED_ARGUMENT]
|
141
|
-
)
|
142
|
-
|
143
|
-
opt.each { |arg,val|
|
144
|
-
case arg
|
145
|
-
when "-d"
|
146
|
-
profiledirectory = val.strip
|
147
|
-
when "-f"
|
148
|
-
mappingfile = val.strip
|
149
|
-
when "-c"
|
150
|
-
existancecheck = true
|
151
|
-
when "-h"
|
152
|
-
help
|
153
|
-
when "-w"
|
154
|
-
pstorefile = val.strip
|
155
|
-
else
|
156
|
-
usage
|
157
|
-
end
|
158
|
-
}
|
159
|
-
usage if mappingfile.nil? || profiledirectory.nil?
|
160
|
-
rescue => err
|
161
|
-
usage
|
162
|
-
end
|
163
|
-
|
164
|
-
profiles = Hash.new
|
165
|
-
duplicates = Hash.new
|
166
|
-
mappings = parse_mapping_file(mappingfile)
|
167
|
-
mappings.each do |uaprof,useragent|
|
168
|
-
begin
|
169
|
-
prof_file = get_uaprofile(uaprof,profiledirectory,existancecheck)
|
170
|
-
uaprof_mapper = UAProfToWURLF.new
|
171
|
-
if profiles.key?(useragent)
|
172
|
-
duplicates[useragent] = Array.new if !duplicates.key?(useragent)
|
173
|
-
duplicates[useragent]<<uaprof
|
174
|
-
next
|
175
|
-
end
|
176
|
-
uaprof_mapper.parse_UAProf("#{profiledirectory}/#{prof_file}")
|
177
|
-
profiles[useragent] = uaprof_mapper
|
178
|
-
rescue => err
|
179
|
-
$stderr.puts "Error: File #{prof_file}; User-Agent:#{useragent}"
|
180
|
-
$stderr.puts "Error:#{err.message}"
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
duplicates.each do |key,profs|
|
185
|
-
$stderr.puts "Duplicates exist for #{key}"
|
186
|
-
profs.each {|prof| $stderr.puts "-- #{prof}" }
|
187
|
-
end
|
188
|
-
|
189
|
-
exit 0 if !pstorefile
|
190
|
-
|
191
|
-
wurflhandsets = load_pstore(pstorefile)
|
192
|
-
|
193
|
-
puts "Comparing WURFL Handsets"
|
194
|
-
profiles.each do |key,val|
|
195
|
-
puts "",""
|
196
|
-
|
197
|
-
if !wurflhandsets.key?(key)
|
198
|
-
puts "UAProf has a new Handset: #{key}"
|
199
|
-
puts "--------------------------------"
|
200
|
-
val.output_WURFL
|
201
|
-
puts "--------------------------------"
|
202
|
-
else
|
203
|
-
uahand = val.make_wurfl_handset
|
204
|
-
res = uahand.compare(wurflhandsets[key])
|
205
|
-
if res.size > 0
|
206
|
-
puts "#{key} : For UAProf and WURFL differ"
|
207
|
-
res.each do |dkey,dval,did|
|
208
|
-
next if did == "generic"
|
209
|
-
#Key UAPROF Value WURFL Value WURFL source id
|
210
|
-
puts " Key:#{dkey}; UVAL:#{uahand[dkey]}; WVAL:#{dval}; WSRCID:#{did}"
|
211
|
-
end
|
212
|
-
#val["user_agent"] = key
|
213
|
-
puts ""
|
214
|
-
puts "WURFL Changes are:"
|
215
|
-
puts ""
|
216
|
-
val.output_WURFL(res.map {|entry| entry[0]})
|
217
|
-
else
|
218
|
-
puts "#{key} : For UAProf and WURFL match"
|
219
|
-
end
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|