View Javadoc

1   package org.rpi.providers;
2   
3   import java.util.Observable;
4   import java.util.Observer;
5   
6   import org.apache.log4j.Logger;
7   import org.openhome.net.device.DvDevice;
8   import org.openhome.net.device.IDvInvocation;
9   import org.openhome.net.device.providers.DvProviderUpnpOrgAVTransport1;
10  import org.rpi.player.PlayManager;
11  import org.rpi.player.events.EventBase;
12  import org.rpi.player.events.EventStatusChanged;
13  import org.rpi.player.events.EventTimeUpdate;
14  import org.rpi.player.events.EventTrackChanged;
15  import org.rpi.player.events.EventUpdateTrackInfo;
16  import org.rpi.playlist.CustomTrack;
17  import org.rpi.utils.Utils;
18  
19  public class PrvAVTransport extends DvProviderUpnpOrgAVTransport1 implements Observer {
20  
21  	private Logger log = Logger.getLogger(PrvAVTransport.class);
22  	private String track_uri = "";
23  	private String track_metadata_html = "";
24  	private String track_metadata = "";
25  	private String track_time = "00:00:00";
26  	private String track_duration = "00:00:00";
27  	private String mStatus = "STOPPED";
28  
29  	public PrvAVTransport(DvDevice iDevice) {
30  		super(iDevice);
31  		log.debug("Creating AvTransport");
32  		enablePropertyLastChange();
33  		// setPropertyLastChange(intitialEvent());
34  		createEvent();
35  		enableActionSetAVTransportURI();
36  		enableActionSetNextAVTransportURI();
37  		enableActionSetPlayMode();
38  		enableActionSetRecordQualityMode();
39  
40  		enableActionGetCurrentTransportActions();
41  		enableActionGetDeviceCapabilities();
42  		enableActionGetMediaInfo();
43  		enableActionGetPositionInfo();
44  		enableActionGetTransportInfo();
45  		enableActionGetTransportSettings();
46  		enableActionSeek();
47  		enableActionGetCurrentTransportActions();
48  
49  		enableActionNext();
50  		enableActionPause();
51  		enableActionPlay();
52  		enableActionPrevious();
53  		// enableActionRecord();
54  
55  		// enableActionSetRecordQualityMode();
56  		enableActionStop();
57  		PlayManager.getInstance().observInfoEvents(this);
58  		PlayManager.getInstance().observTimeEvents(this);
59  		PlayManager.getInstance().observPlayListEvents(this);
60  		PlayManager.getInstance().observAVEvnts(this);
61  
62  	}
63  
64  	private void createEventState() {
65  		StringBuilder sb = new StringBuilder();
66  		sb.append("<Event xmlns = \"urn:schemas-upnp-org:metadata-1-0/AVT/\">");
67  		sb.append("<InstanceID val=\"0\">");
68  		sb.append("<TransportState val=\"" + mStatus.toUpperCase() + "\"/>");
69  		sb.append("</InstanceID>");
70  		sb.append("</Event>");
71  		setPropertyLastChange(sb.toString());
72  	}
73  
74  	/**
75  	 * Create a big Event that sets all our variables
76  	 */
77  	/*
78  	 * private void createEvent() { StringBuilder sb = new StringBuilder();
79  	 * sb.append("<Event xmlns = \"urn:schemas-upnp-org:metadata-1-0/AVT/\">");
80  	 * sb.append("<InstanceID val=\"0\">");
81  	 * sb.append("	<CurrentPlayMode val=\"NORMAL\"/>");
82  	 * sb.append("<RecordStorageMedium val=\"NOT_IMPLEMENTED\"/>");
83  	 * sb.append("<CurrentTrackURI	 val=\"" + track_uri + "\"/>");
84  	 * //sb.append("<CurrentTrackDuration val=\"" + track_duration + "\"/>");
85  	 * sb.append("<CurrentRecordQualityMode val=\"NOT_IMPLEMENTED\"/>");
86  	 * sb.append("<CurrentMediaDuration val=\"" + track_time + "\"/>");
87  	 * sb.append("<AVTransportURI val=\"\"/>");
88  	 * sb.append("<TransportState val=\"" + mStatus.toUpperCase() + "\"/>");
89  	 * sb.append("<CurrentTrackMetaData val=\"" + track_metadata_html + "\"/>");
90  	 * sb.append("<NextAVTransportURI val=\"NOT_IMPLEMENTED\"/>");
91  	 * sb.append("<PossibleRecordQualityModes val=\"NOT_IMPLEMENTED\"/>");
92  	 * sb.append("<CurrentTrack val=\"0\" />");
93  	 * sb.append("<NextAVTransportURIMetaData val=\"NOT_IMPLEMENTED\"/>");
94  	 * sb.append("<PlaybackStorageMedium val=\"NONE\"/>"); sb.append(
95  	 * "<CurrentTransportActions val=\"Play,Pause,Stop,Seek,Next,Previous\"/>");
96  	 * sb.append("<RecordMediumWriteStatus val=\"NOT_IMPLEMENTED\"/>");
97  	 * sb.append
98  	 * ("<PossiblePlaybackStorageMedia val=\"NONE,NETWORK,HDD,CD-DA,UNKNOWN\"/>"
99  	 * ); sb.append("<AVTransportURIMetaData val=\"\"/>");
100 	 * sb.append("<NumberOfTracks val=\"0\" />");
101 	 * sb.append("<PossibleRecordStorageMedia val=\"NOT_IMPLEMENTED\"/>");
102 	 * sb.append("<TransportStatus val=\"OK\"/>");
103 	 * sb.append("<TransportPlaySpeed val=\"1\"/>"); sb.append("</InstanceID>");
104 	 * sb.append("</Event>"); setPropertyLastChange(sb.toString()); }
105 	 */
106 
107 	private void createEvent() {
108 		StringBuilder sb = new StringBuilder();
109 		sb.append("<Event xmlns=\"urn:schemas-upnp-org:metadata-1-0/AVT/\">");
110 		sb.append("<InstanceID val=\"0\">");
111 		sb.append("<CurrentPlayMode val=\"NORMAL\" />");
112 		sb.append("<RecordStorageMedium val=\"NOT_IMPLEMENTED\" />");
113 		// sb.append("<CurrentTrackURI	val=\"http://192.168.1.205:26125/content/c2/b16/f44100/d93277-co12.mp3\" />");
114 		sb.append("<CurrentTrackURI	val=\"" + track_uri + "\" />");
115 		sb.append("<CurrentTrackDuration val=\"" + track_duration + "\" />");
116 		sb.append("<CurrentRecordQualityMode val=\"NOT_IMPLEMENTED\" />");
117 		sb.append("<CurrentMediaDuration val=\"00:00:00\" />");
118 		sb.append("<AVTransportURI val=\"\" />");
119 		sb.append("<TransportState val=\"" + mStatus.toUpperCase() + "\" />");
120 		// sb.append("<CurrentTrackMetaData val=\"&lt;DIDL-Lite xmlns=&quot;urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/&quot;>&lt;item id=&quot;d93277-co12&quot; parentID=&quot;co12&quot; restricted=&quot;0&quot;>&lt;dc:title xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot;>Te Siento (Version espagnole de ''Ti Sento'')&lt;/dc:title>&lt;dc:creator xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot;>Matia Bazar&lt;/dc:creator>&lt;dc:date xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot;>2011-01-01&lt;/dc:date>&lt;upnp:artist role=&quot;AlbumArtist&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;>Matia Bazar&lt;/upnp:artist>&lt;upnp:artist role=&quot;Performer&quot; xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;>Matia Bazar&lt;/upnp:artist>&lt;upnp:album xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;>Fantasia - Best &amp;amp; Rarities&lt;/upnp:album>&lt;upnp:genre xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;>Italian Pop&lt;/upnp:genre>&lt;upnp:originalTrackNumber xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;>8&lt;/upnp:originalTrackNumber>&lt;upnp:originalDiscNumber xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;>2&lt;/upnp:originalDiscNumber>&lt;res duration=&quot;0:04:06.000&quot; size=&quot;9855919&quot; bitrate=&quot;40006&quot; bitsPerSample=&quot;16&quot; sampleFrequency=&quot;44100&quot; nrAudioChannels=&quot;2&quot; protocolInfo=&quot;http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01&quot;>http://192.168.1.205:26125/content/c2/b16/f44100/d93277-co12.mp3&lt;/res>&lt;res duration=&quot;0:04:06.000&quot; size=&quot;43457948&quot; bitrate=&quot;176400&quot; bitsPerSample=&quot;16&quot; sampleFrequency=&quot;44100&quot; nrAudioChannels=&quot;2&quot; protocolInfo=&quot;http-get:*:audio/wav:DLNA.ORG_PN=WAV;DLNA.ORG_OP=01&quot;>http://192.168.1.205:26125/content/c2/b16/f44100/d93277-co12.forced.wav&lt;/res>&lt;res duration=&quot;0:04:06.000&quot; size=&quot;43457904&quot; bitrate=&quot;176400&quot; bitsPerSample=&quot;16&quot; sampleFrequency=&quot;44100&quot; nrAudioChannels=&quot;2&quot; protocolInfo=&quot;http-get:*:audio/L16:DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_CI=1&quot;>http://192.168.1.205:26125/content/c2/b16/f44100/d93277-co12.forced.l16&lt;/res>&lt;res duration=&quot;0:04:06.000&quot; size=&quot;9855919&quot; bitrate=&quot;40006&quot; bitsPerSample=&quot;16&quot; sampleFrequency=&quot;44100&quot; nrAudioChannels=&quot;2&quot; protocolInfo=&quot;http-get:*:audio/mpeg:DLNA.ORG_PN=MP3;DLNA.ORG_OP=01&quot;>http://192.168.1.205:26125/content/c2/b16/f44100/d93277-co12.mp3&lt;/res>&lt;upnp:class xmlns:upnp=&quot;urn:schemas-upnp-org:metadata-1-0/upnp/&quot;>object.item.audioItem.musicTrack&lt;/upnp:class>&lt;/item>&lt;/DIDL-Lite>\" />");
121 		sb.append("<CurrentTrackMetaData val=\"" + track_metadata_html + "\" />");
122 		sb.append("<NextAVTransportURI val=\"NOT_IMPLEMENTED\" />");
123 		sb.append("<PossibleRecordQualityModes val=\"NOT_IMPLEMENTED\" />");
124 		sb.append("<CurrentTrack val=\"0\" />");
125 		sb.append("<NextAVTransportURIMetaData val=\"NOT_IMPLEMENTED\" />");
126 		sb.append("<PlaybackStorageMedium val=\"NONE\" />");
127 		sb.append("<CurrentTransportActions val=\"Play,Pause,Stop,Seek,Next,Previous\" />");
128 		sb.append("<RecordMediumWriteStatus val=\"NOT_IMPLEMENTED\" />");
129 		sb.append("<PossiblePlaybackStorageMedia val=\"NONE,NETWORK,HDD,CD-DA,UNKNOWN\" />");
130 		sb.append("<AVTransportURIMetaData val=\"\" />");
131 		sb.append("<NumberOfTracks val=\"1\" />");
132 		sb.append("<PossibleRecordStorageMedia val=\"NOT_IMPLEMENTED\" />");
133 		sb.append("<TransportStatus val=\"OK\" />");
134 		sb.append("<TransportPlaySpeed val=\"1\" />");
135 		sb.append("</InstanceID>");
136 		sb.append("</Event>");
137 		setPropertyLastChange(sb.toString());
138 	}
139 
140 	@Override
141 	protected String getCurrentTransportActions(IDvInvocation paramIDvInvocation, long paramLong) {
142 		log.debug("Transport Actions" + Utils.getLogText(paramIDvInvocation));
143 		return "Play,Pause,Stop,Seek,Next,Previous";
144 		// return "";
145 	}
146 
147 	@Override
148 	protected GetDeviceCapabilities getDeviceCapabilities(IDvInvocation paramIDvInvocation, long paramLong) {
149 		log.debug("GetDevice Capabilities" + Utils.getLogText(paramIDvInvocation));
150 		String cap = "vendor-defined ,NOT_IMPLEMENTED,NONE,NETWORK,MICRO-MV,HDD,LD,DAT,DVD-AUDIO,DVD-RAM,DVD-RW,DVD+RW,DVD-R,DVD-VIDEO,DVD-ROM,MD-PICTURE,MD-AUDIO,SACD,VIDEO-CD,CD-RW,CD-R,CD-DA,CD-ROM,HI8,VIDEO8,VHSC,D-VHS,S-VHS,W-VHS,VHS,MINI-DV,DV,UNKNOWN";
151 		String rec = " vendor-defined ,NOT_IMPLEMENTED,2:HIGH,1:MEDIUM,0:BASIC,2:SP,1:LP,0:EP";
152 		// GetDeviceCapabilities dev = new
153 		// GetDeviceCapabilities("NONE,NETWORK,HDD,CD-DA,UNKNOWN",
154 		// "NOT_IMPLEMENTED", "NOT_IMPLEMENTED");
155 		GetDeviceCapabilities dev = new GetDeviceCapabilities(cap, cap, rec);
156 		return dev;
157 
158 	}
159 
160 	@Override
161 	protected GetMediaInfo getMediaInfo(IDvInvocation paramIDvInvocation, long paramLong) {
162 		log.debug("GetMediaInfo" + Utils.getLogText(paramIDvInvocation));
163 		// createEvent();
164 		GetMediaInfo info = new GetMediaInfo(0, track_duration, track_uri, track_metadata, "", "", "UNKNOWN", "UNKNOWN", "UNKNOWN");
165 		// GetMediaInfo info = new GetMediaInfo(0, "00:00:00", "", "", "", "",
166 		// "UNKNOWN", "UNKNOWN", "UNKNOWN");
167 		return info;
168 	}
169 
170 	@Override
171 	protected GetPositionInfo getPositionInfo(IDvInvocation paramIDvInvocation, long paramLong) {
172 		log.debug("Get Position Info" + Utils.getLogText(paramIDvInvocation));
173 		GetPositionInfo info = new GetPositionInfo(0, track_duration, track_metadata, track_uri, track_time, "00:00:00", 2147483647, 2147483647);
174 		// GetPositionInfo info = new GetPositionInfo(1, "00:00:00", "", "",
175 		// "00:00:00", "NOT_IMPLEMENTED", 2147483647, 2147483647);
176 		return info;
177 	}
178 
179 	@Override
180 	protected GetTransportInfo getTransportInfo(IDvInvocation paramIDvInvocation, long paramLong) {
181 		log.debug("GetTransport Info" + Utils.getLogText(paramIDvInvocation));
182 		// createEvent();
183 		GetTransportInfo info = new GetTransportInfo(mStatus.toUpperCase(), "OK", "1");
184 		return info;
185 	}
186 
187 	@Override
188 	protected GetTransportSettings getTransportSettings(IDvInvocation paramIDvInvocation, long paramLong) {
189 		log.debug("GetTransportSettings" + Utils.getLogText(paramIDvInvocation));
190 		GetTransportSettings settings = new GetTransportSettings("NORMAL", "0:BASIC");
191 		return settings;
192 	}
193 
194 	@Override
195 	protected void next(IDvInvocation paramIDvInvocation, long paramLong) {
196 		log.debug("Next: " + paramLong + Utils.getLogText(paramIDvInvocation));
197 		PlayManager.getInstance().nextTrack();
198 	}
199 
200 	@Override
201 	protected void pause(IDvInvocation paramIDvInvocation, long paramLong) {
202 		log.debug("Pause" + paramLong + Utils.getLogText(paramIDvInvocation));
203 		PlayManager.getInstance().pause();
204 	}
205 
206 	@Override
207 	protected void play(IDvInvocation paramIDvInvocation, long paramLong, String paramString) {
208 		log.debug("Play: " + paramString + Utils.getLogText(paramIDvInvocation));
209 		if (!track_uri.equalsIgnoreCase("")) {
210 			if (mStatus.equalsIgnoreCase("PAUSED_PLAYBACK")) {
211 				PlayManager.getInstance().play();
212 			} else {
213 				CustomTrack c = new CustomTrack(track_uri, track_metadata, 0);
214 				PlayManager.getInstance().playAV(c);
215 			}
216 		} else {
217 			if (mStatus.equalsIgnoreCase("PAUSED_PLAYBACK")) {
218 				PlayManager.getInstance().play();
219 			}
220 		}
221 
222 	}
223 
224 	@Override
225 	protected void previous(IDvInvocation paramIDvInvocation, long paramLong) {
226 		log.debug("Previous: " + paramLong + Utils.getLogText(paramIDvInvocation));
227 		PlayManager.getInstance().previousTrack();
228 	}
229 
230 	@Override
231 	protected void record(IDvInvocation paramIDvInvocation, long paramLong) {
232 		log.debug("Record");
233 	}
234 
235 	@Override
236 	protected void seek(IDvInvocation paramIDvInvocation, long paramLong, String paramString1, String paramString2) {
237 		log.debug("Seek: " + paramString2 + Utils.getLogText(paramIDvInvocation));
238 		PlayManager.getInstance().seekAbsolute(getSeconds(paramString2));
239 	}
240 
241 	private long getSeconds(String t) {
242 		// String timestampStr = "14:35:06";
243 		int duration = 0;
244 		try {
245 			String[] tokens = t.split(":");
246 			int hours = Integer.parseInt(tokens[0]);
247 			int minutes = Integer.parseInt(tokens[1]);
248 			int seconds = Integer.parseInt(tokens[2]);
249 			duration = 3600 * hours + 60 * minutes + seconds;
250 		} catch (Exception e) {
251 
252 		}
253 		return (long) duration;
254 	}
255 
256 	@Override
257 	protected void setAVTransportURI(IDvInvocation paramIDvInvocation, long paramLong, String url, String meta_data) {
258 		log.debug("SetAVTransport: " + paramLong + " URL: " + url + " MetaData: " + meta_data + Utils.getLogText(paramIDvInvocation));
259 		track_uri = url;
260 		track_metadata = meta_data;
261 	}
262 
263 	@Override
264 	protected void setNextAVTransportURI(IDvInvocation paramIDvInvocation, long paramLong, String paramString1, String paramString2) {
265 		log.debug("SetNexAVTransport: " + paramLong + " " + paramString1 + " " + paramString2 + Utils.getLogText(paramIDvInvocation));
266 	}
267 
268 	@Override
269 	protected void setPlayMode(IDvInvocation paramIDvInvocation, long paramLong, String paramString) {
270 		log.debug("SetPlayMode: " + paramLong + " " + paramString + Utils.getLogText(paramIDvInvocation));
271 	}
272 
273 	@Override
274 	protected void setRecordQualityMode(IDvInvocation paramIDvInvocation, long paramLong, String paramString) {
275 		log.debug("SetRecordQuality: " + paramLong + " " + paramString + Utils.getLogText(paramIDvInvocation));
276 	}
277 
278 	@Override
279 	protected void stop(IDvInvocation paramIDvInvocation, long paramLong) {
280 		log.debug("Stop: " + paramLong + Utils.getLogText(paramIDvInvocation));
281 		PlayManager.getInstance().stop();
282 	}
283 
284 	@Override
285 	public void update(Observable arg0, Object ev) {
286 		EventBase e = (EventBase) ev;
287 		switch (e.getType()) {
288 		case EVENTTRACKCHANGED:
289 			EventTrackChanged ec = (EventTrackChanged) e;
290 			CustomTrack track = ec.getTrack();
291 			String m_uri = "";
292 			String m_metadata = "";
293 			if (track != null) {
294 				m_uri = track.getUri();
295 				m_metadata = track.getMetadata();
296 				if (!(m_uri.equalsIgnoreCase(track_uri)) || (!m_metadata.equalsIgnoreCase(track_metadata))) {
297 					track_uri = m_uri;
298 					track_metadata = m_metadata;
299 					track_metadata_html = stringToHTMLString(m_metadata);
300 					createEvent();
301 				}
302 			} else {
303 				// m_uri = "";
304 				// m_metadata = "";
305 			}
306 
307 			break;
308 		case EVENTTIMEUPDATED:
309 			EventTimeUpdate etime = (EventTimeUpdate) e;
310 			track_time = ConvertTime(etime.getTime());
311 			// createEvent();
312 			break;
313 		case EVENTUPDATETRACKINFO:
314 			EventUpdateTrackInfo eti = (EventUpdateTrackInfo) e;
315 			track_duration = ConvertTime(eti.getTrackInfo().getDuration());
316 			createEvent();
317 			break;
318 		// case EVENTPLAYLISTSTATUSCHANGED:
319 		// EventPlayListStatusChanged eps = (EventPlayListStatusChanged) e;
320 		// String status = eps.getStatus();
321 		// if (status != null) {
322 		// if (status.equalsIgnoreCase("PAUSED")) {
323 		// status = "PAUSED_PLAYBACK";
324 		// }
325 		// if (!mStatus.equalsIgnoreCase(status)) {
326 		// createEvent();
327 		// }
328 		// mStatus = status;
329 		// }
330 		case EVENTSTATUSCHANGED:
331 			EventStatusChanged esc = (EventStatusChanged) e;
332 			String statuss = esc.getStatus();
333 			log.debug("Status: " + statuss);
334 			if (statuss != null) {
335 				if (statuss.equalsIgnoreCase("PAUSED")) {
336 					statuss = "PAUSED_PLAYBACK";
337 				}
338 				if (statuss.equalsIgnoreCase("BUFFERING")) {
339 					statuss = "TRANSITIONING";
340 				}
341 				if (!mStatus.equalsIgnoreCase(statuss)) {
342 					mStatus = statuss;
343 					createEvent();
344 				}
345 			}
346 		default:
347 		}
348 	}
349 
350 	/***
351 	 * Convert seconds to Hours:Seconds
352 	 * 
353 	 * @param lTime
354 	 * @return
355 	 */
356 	private String ConvertTime(long lTime) {
357 		int seconds = (int) lTime;
358 		int hours = seconds / 3600;
359 		int minutes = (seconds % 3600) / 60;
360 		seconds = seconds % 60;
361 
362 		return twoDigitString(hours) + ":" + twoDigitString(minutes) + ":" + twoDigitString(seconds);
363 	}
364 
365 	private String twoDigitString(int number) {
366 
367 		if (number < 0) {
368 			number = 0;
369 		}
370 		if (number == 0) {
371 			return "00";
372 		}
373 
374 		if (number / 10 == 0) {
375 			return "0" + number;
376 		}
377 
378 		return String.valueOf(number);
379 	}
380 
381 	public static String stringToHTMLString(String string) {
382 		StringBuffer sb = new StringBuffer(string.length());
383 		// true if last char was blank
384 		boolean lastWasBlankChar = false;
385 		int len = string.length();
386 		char c;
387 
388 		for (int i = 0; i < len; i++) {
389 			c = string.charAt(i);
390 			if (c == ' ') {
391 				// blank gets extra work,
392 				// this solves the problem you get if you replace all
393 				// blanks with &nbsp;, if you do that you loss
394 				// word breaking
395 				if (lastWasBlankChar) {
396 					lastWasBlankChar = false;
397 					sb.append("&nbsp;");
398 				} else {
399 					lastWasBlankChar = true;
400 					sb.append(' ');
401 				}
402 			} else {
403 				lastWasBlankChar = false;
404 				//
405 				// HTML Special Chars
406 				if (c == '"')
407 					sb.append("&quot;");
408 				else if (c == '&')
409 					sb.append("&amp;");
410 				else if (c == '<')
411 					sb.append("&lt;");
412 				else if (c == '>')
413 					sb.append("&gt;");
414 				else if (c == '\n')
415 					// Handle Newline
416 					sb.append("&lt;br/&gt;");
417 				else {
418 					int ci = 0xffff & c;
419 					if (ci < 160)
420 						// nothing special only 7 Bit
421 						sb.append(c);
422 					else {
423 						// Not 7 Bit use the unicode system
424 						sb.append("&#");
425 						sb.append(new Integer(ci).toString());
426 						sb.append(';');
427 					}
428 				}
429 			}
430 		}
431 		return sb.toString();
432 	}
433 
434 }