Gerer les souscription aux variables

This commit is contained in:
2025-10-09 07:00:48 +02:00
parent e5c83d01c0
commit fd1a44b6fa
3 changed files with 140 additions and 109 deletions

View File

@@ -298,7 +298,7 @@ pub trait WebAppExt {
/// # Type Parameter /// # Type Parameter
/// ///
/// * `W` - Type RustEmbed contenant les fichiers de la webapp /// * `W` - Type RustEmbed contenant les fichiers de la webapp
fn add_webapp<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> async fn add_webapp<W>(&mut self, path: &str)
where where
W: RustEmbed + Clone + Send + Sync + 'static; W: RustEmbed + Clone + Send + Sync + 'static;
@@ -311,7 +311,7 @@ pub trait WebAppExt {
/// # Type Parameter /// # Type Parameter
/// ///
/// * `W` - Type RustEmbed contenant les fichiers de la webapp /// * `W` - Type RustEmbed contenant les fichiers de la webapp
fn add_webapp_with_redirect<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> async fn add_webapp_with_redirect<W>(&mut self, path: &str)
where where
W: RustEmbed + Clone + Send + Sync + 'static; W: RustEmbed + Clone + Send + Sync + 'static;
} }

View File

@@ -30,28 +30,24 @@
use crate::WebAppExt; use crate::WebAppExt;
use pmoserver::Server; use pmoserver::Server;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::future::Future;
use std::pin::Pin;
impl WebAppExt for Server { impl WebAppExt for Server {
fn add_webapp<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> async fn add_webapp<W>(&mut self, path: &str)
where where
W: RustEmbed + Clone + Send + Sync + 'static, W: RustEmbed + Clone + Send + Sync + 'static,
{ {
let path = path.to_string(); let path = path.to_string();
Box::pin(async move {
self.add_spa::<W>(&path).await; self.add_spa::<W>(&path).await;
})
} }
fn add_webapp_with_redirect<W>(&mut self, path: &str) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> async fn add_webapp_with_redirect<W>(&mut self, path: &str)
where where
W: RustEmbed + Clone + Send + Sync + 'static, W: RustEmbed + Clone + Send + Sync + 'static,
{ {
let path = path.to_string(); let path = path.to_string();
Box::pin(async move {
self.add_spa::<W>(&path).await; self.add_spa::<W>(&path).await;
self.add_redirect("/", &path).await; self.add_redirect("/", &path).await;
})
} }
} }

View File

@@ -1,28 +1,28 @@
//! Implémentation de ServiceInstance. //! Implémentation de ServiceInstance.
use std::{
collections::HashMap,
sync::{Arc, Mutex, RwLock},
time::Duration,
pin::Pin,
future::Future,
};
use axum::{ use axum::{
body::Body,
extract::{Request, State}, extract::{Request, State},
http::{HeaderMap, StatusCode}, http::{HeaderMap, StatusCode},
response::{IntoResponse, Response}, response::{IntoResponse, Response},
body::Body, };
use std::{
collections::HashMap,
future::Future,
pin::Pin,
sync::{Arc, Mutex, RwLock},
time::Duration,
}; };
use tokio::time; use tokio::time;
use tracing::{info, warn, error}; use tracing::{error, info, warn};
use xmltree::{Element, XMLNode, EmitterConfig}; use xmltree::{Element, EmitterConfig, XMLNode};
use crate::{ use crate::{
services::{Service, ServiceError}, UpnpInstance, UpnpObject, UpnpObjectType, UpnpTyped, UpnpTypedInstance,
actions::{ActionInstance, ActionInstanceSet}, actions::{ActionInstance, ActionInstanceSet},
state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable},
devices::DeviceInstance, devices::DeviceInstance,
UpnpObject, UpnpInstance, UpnpTyped, UpnpTypedInstance, UpnpObjectType, services::{Service, ServiceError},
state_variables::{StateVarInstance, StateVarInstanceSet, UpnpVariable},
}; };
/// Méthodes HTTP pour les événements UPnP. /// Méthodes HTTP pour les événements UPnP.
@@ -70,33 +70,32 @@ pub const METHOD_UNSUBSCRIBE: &str = "UNSUBSCRIBE";
pub struct ServiceInstance { pub struct ServiceInstance {
/// Métadonnées de l'objet /// Métadonnées de l'objet
object: UpnpObjectType, object: UpnpObjectType,
/// Référence vers le modèle /// Référence vers le modèle
model: Arc<Service>, model: Arc<Service>,
/// Identifiant du service /// Identifiant du service
identifier: String, identifier: String,
/// Device parent (optionnel) - utilisé via interior mutability /// Device parent (optionnel) - utilisé via interior mutability
device: Arc<RwLock<Option<Arc<DeviceInstance>>>>, device: Arc<RwLock<Option<Arc<DeviceInstance>>>>,
/// Variables d'état instanciées /// Variables d'état instanciées
statevariables: StateVarInstanceSet, statevariables: StateVarInstanceSet,
/// Actions instanciées /// Actions instanciées
actions: ActionInstanceSet, actions: ActionInstanceSet,
/// Abonnés aux événements (SID -> Callback URL) /// Abonnés aux événements (SID -> Callback URL)
subscribers: Arc<RwLock<HashMap<String, String>>>, subscribers: Arc<RwLock<HashMap<String, String>>>,
/// Buffer des changements en attente de notification /// Buffer des changements en attente de notification
changed_buffer: Arc<Mutex<HashMap<String, String>>>, changed_buffer: Arc<Mutex<HashMap<String, String>>>,
/// Compteurs de séquence par abonné /// Compteurs de séquence par abonné
seqid: Arc<Mutex<HashMap<String, u32>>>, seqid: Arc<Mutex<HashMap<String, u32>>>,
} }
impl std::fmt::Debug for ServiceInstance { impl std::fmt::Debug for ServiceInstance {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceInstance") f.debug_struct("ServiceInstance")
@@ -132,14 +131,14 @@ impl UpnpInstance for ServiceInstance {
for a in model.actions() { for a in model.actions() {
// Vérifier que toutes les variables référencées existent // Vérifier que toutes les variables référencées existent
let mut missing_vars = Vec::new(); let mut missing_vars = Vec::new();
for arg in a.arguments().all() { for arg in a.arguments().all() {
let related_var_name = arg.state_variable().get_name(); let related_var_name = arg.state_variable().get_name();
if statevariables.get_by_name(related_var_name).is_none() { if statevariables.get_by_name(related_var_name).is_none() {
missing_vars.push(related_var_name.to_string()); missing_vars.push(related_var_name.to_string());
} }
} }
if !missing_vars.is_empty() { if !missing_vars.is_empty() {
error!( error!(
"Action '{}' references missing state variables: {:?}", "Action '{}' references missing state variables: {:?}",
@@ -148,10 +147,10 @@ impl UpnpInstance for ServiceInstance {
); );
continue; continue;
} }
// Créer l'instance d'action // Créer l'instance d'action
let action_instance = Arc::new(ActionInstance::new(&*a)); let action_instance = Arc::new(ActionInstance::new(&*a));
// ✅ Phase 3 : ACTIVER le binding des arguments aux variables d'instance // ✅ Phase 3 : ACTIVER le binding des arguments aux variables d'instance
for arg_instance in action_instance.arguments_set().all() { for arg_instance in action_instance.arguments_set().all() {
let var_name = arg_instance.get_model().state_variable().get_name(); let var_name = arg_instance.get_model().state_variable().get_name();
@@ -160,7 +159,7 @@ impl UpnpInstance for ServiceInstance {
arg_instance.bind_variable(var_instance); arg_instance.bind_variable(var_instance);
} }
} }
if let Err(e) = actions.insert(action_instance) { if let Err(e) = actions.insert(action_instance) {
error!("Failed to insert action '{}': {:?}", a.get_name(), e); error!("Failed to insert action '{}': {:?}", a.get_name(), e);
} }
@@ -194,7 +193,9 @@ impl UpnpObject for ServiceInstance {
let mut elem = Element::new("service"); let mut elem = Element::new("service");
let mut service_type = Element::new("serviceType"); let mut service_type = Element::new("serviceType");
service_type.children.push(XMLNode::Text(self.service_type())); service_type
.children
.push(XMLNode::Text(self.service_type()));
elem.children.push(XMLNode::Element(service_type)); elem.children.push(XMLNode::Element(service_type));
let mut service_id = Element::new("serviceId"); let mut service_id = Element::new("serviceId");
@@ -206,11 +207,15 @@ impl UpnpObject for ServiceInstance {
elem.children.push(XMLNode::Element(scpd_url)); elem.children.push(XMLNode::Element(scpd_url));
let mut control_url = Element::new("controlURL"); let mut control_url = Element::new("controlURL");
control_url.children.push(XMLNode::Text(self.control_route())); control_url
.children
.push(XMLNode::Text(self.control_route()));
elem.children.push(XMLNode::Element(control_url)); elem.children.push(XMLNode::Element(control_url));
let mut event_sub_url = Element::new("eventSubURL"); let mut event_sub_url = Element::new("eventSubURL");
event_sub_url.children.push(XMLNode::Text(self.event_route())); event_sub_url
.children
.push(XMLNode::Text(self.event_route()));
elem.children.push(XMLNode::Element(event_sub_url)); elem.children.push(XMLNode::Element(event_sub_url));
elem elem
@@ -241,7 +246,7 @@ impl ServiceInstance {
pub fn get_variable(&self, name: &str) -> Option<Arc<StateVarInstance>> { pub fn get_variable(&self, name: &str) -> Option<Arc<StateVarInstance>> {
self.statevariables.get_by_name(name) self.statevariables.get_by_name(name)
} }
/// Raccourci pour obtenir une action par nom /// Raccourci pour obtenir une action par nom
pub fn get_action(&self, name: &str) -> Option<Arc<ActionInstance>> { pub fn get_action(&self, name: &str) -> Option<Arc<ActionInstance>> {
self.actions.get_by_name(name) self.actions.get_by_name(name)
@@ -306,8 +311,14 @@ impl ServiceInstance {
/// Retourne une erreur si l'enregistrement des routes échoue. /// Retourne une erreur si l'enregistrement des routes échoue.
pub async fn register_urls(&self, server: &mut pmoserver::Server) -> Result<(), ServiceError> { pub async fn register_urls(&self, server: &mut pmoserver::Server) -> Result<(), ServiceError> {
let device = self.device.read().unwrap(); let device = self.device.read().unwrap();
let device_name = device.as_ref().map(|d| d.get_name().clone()).unwrap_or_else(|| "unknown".to_string()); let device_name = device
let server_url = device.as_ref().map(|d| d.base_url().to_string()).unwrap_or_default(); .as_ref()
.map(|d| d.get_name().clone())
.unwrap_or_else(|| "unknown".to_string());
let server_url = device
.as_ref()
.map(|d| d.base_url().to_string())
.unwrap_or_default();
drop(device); drop(device);
info!( info!(
@@ -320,26 +331,24 @@ impl ServiceInstance {
// Handler SCPD // Handler SCPD
let instance_scpd = self.clone(); let instance_scpd = self.clone();
server.add_handler(&self.scpd_route(), move || { server
let instance = instance_scpd.clone(); .add_handler(&self.scpd_route(), move || {
async move { instance.scpd_handler().await } let instance = instance_scpd.clone();
}).await; async move { instance.scpd_handler().await }
})
.await;
// Handler control // Handler control
let instance_control = self.clone(); let instance_control = self.clone();
server.add_post_handler_with_state( server
&self.control_route(), .add_post_handler_with_state(&self.control_route(), control_handler, instance_control)
control_handler, .await;
instance_control,
).await;
// Handler événements // Handler événements
let instance_event = self.clone(); let instance_event = self.clone();
server.add_handler_with_state( server
&self.event_route(), .add_handler_with_state(&self.event_route(), event_sub_handler, instance_event)
event_sub_handler, .await;
instance_event,
).await;
Ok(()) Ok(())
} }
@@ -357,25 +366,23 @@ impl ServiceInstance {
let mut major = Element::new("major"); let mut major = Element::new("major");
major.children.push(XMLNode::Text("1".to_string())); major.children.push(XMLNode::Text("1".to_string()));
spec.children.push(XMLNode::Element(major)); spec.children.push(XMLNode::Element(major));
let mut minor = Element::new("minor"); let mut minor = Element::new("minor");
minor.children.push(XMLNode::Text("0".to_string())); minor.children.push(XMLNode::Text("0".to_string()));
spec.children.push(XMLNode::Element(minor)); spec.children.push(XMLNode::Element(minor));
elem.children.push(XMLNode::Element(spec)); elem.children.push(XMLNode::Element(spec));
// actionList // actionList
if !self.actions.all().is_empty() { if !self.actions.all().is_empty() {
elem.children.push(XMLNode::Element( elem.children
self.actions.to_xml_element() .push(XMLNode::Element(self.actions.to_xml_element()));
));
} }
// serviceStateTable // serviceStateTable
if !self.statevariables.all().is_empty() { if !self.statevariables.all().is_empty() {
elem.children.push(XMLNode::Element( elem.children
self.statevariables.to_xml_element() .push(XMLNode::Element(self.statevariables.to_xml_element()));
));
} }
elem elem
@@ -384,11 +391,11 @@ impl ServiceInstance {
/// Handler pour la description SCPD. /// Handler pour la description SCPD.
async fn scpd_handler(&self) -> Response { async fn scpd_handler(&self) -> Response {
let elem = self.scpd_element(); let elem = self.scpd_element();
let config = EmitterConfig::new() let config = EmitterConfig::new()
.perform_indent(true) .perform_indent(true)
.indent_string(" "); .indent_string(" ");
let mut xml_output = Vec::new(); let mut xml_output = Vec::new();
if let Err(e) = elem.write_with_config(&mut xml_output, config) { if let Err(e) = elem.write_with_config(&mut xml_output, config) {
error!("Failed to serialize SCPD XML: {}", e); error!("Failed to serialize SCPD XML: {}", e);
@@ -399,9 +406,13 @@ impl ServiceInstance {
( (
StatusCode::OK, StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], [(
axum::http::header::CONTENT_TYPE,
"text/xml; charset=\"utf-8\"",
)],
xml, xml,
).into_response() )
.into_response()
} }
/// Ajoute un abonné aux événements. /// Ajoute un abonné aux événements.
@@ -442,10 +453,14 @@ impl ServiceInstance {
tokio::spawn(async move { tokio::spawn(async move {
let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); let callback = callback.trim().trim_matches(|c| c == '<' || c == '>');
let mut body = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string(); let mut body =
r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
for (name, val) in changed { for (name, val) in changed {
body.push_str(&format!("<e:property><{0}>{1}</{0}></e:property>", name, val)); body.push_str(&format!(
"<e:property><{0}>{1}</{0}></e:property>",
name, val
));
} }
body.push_str("</e:propertyset>"); body.push_str("</e:propertyset>");
@@ -462,7 +477,11 @@ impl ServiceInstance {
.await .await
{ {
Ok(resp) => { Ok(resp) => {
info!("✅ Initial event sent to {}, status={}", callback, resp.status()); info!(
"✅ Initial event sent to {}, status={}",
callback,
resp.status()
);
} }
Err(e) => { Err(e) => {
error!("Failed to send initial event to {}: {}", callback, e); error!("Failed to send initial event to {}: {}", callback, e);
@@ -507,13 +526,17 @@ impl ServiceInstance {
for (sid, callback) in subscribers_copy { for (sid, callback) in subscribers_copy {
let changed_clone = changed.clone(); let changed_clone = changed.clone();
let seq = self.next_seq(&sid); let seq = self.next_seq(&sid);
tokio::spawn(async move { tokio::spawn(async move {
let callback = callback.trim().trim_matches(|c| c == '<' || c == '>'); let callback = callback.trim().trim_matches(|c| c == '<' || c == '>');
let mut body = r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string(); let mut body =
r#"<e:propertyset xmlns:e="urn:schemas-upnp-org:event-1-0">"#.to_string();
for (name, val) in changed_clone { for (name, val) in changed_clone {
body.push_str(&format!("<e:property><{0}>{1}</{0}></e:property>", name, val)); body.push_str(&format!(
"<e:property><{0}>{1}</{0}></e:property>",
name, val
));
} }
body.push_str("</e:propertyset>"); body.push_str("</e:propertyset>");
@@ -551,7 +574,7 @@ impl ServiceInstance {
/// Un handle vers la tâche tokio du notifier. /// Un handle vers la tâche tokio du notifier.
pub fn start_notifier(&self, interval: Duration) -> tokio::task::JoinHandle<()> { pub fn start_notifier(&self, interval: Duration) -> tokio::task::JoinHandle<()> {
let instance = self.clone(); let instance = self.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut ticker = time::interval(interval); let mut ticker = time::interval(interval);
info!("✅ Starting notifier every {:?}", interval); info!("✅ Starting notifier every {:?}", interval);
@@ -565,18 +588,26 @@ impl ServiceInstance {
} }
/// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE). /// Handler Axum pour les événements (SUBSCRIBE/UNSUBSCRIBE).
fn event_sub_handler( async fn event_sub_handler(
State(instance): State<ServiceInstance>, State(instance): State<ServiceInstance>,
headers: HeaderMap, headers: HeaderMap,
req: Request<Body>, req: Request<Body>,
) -> Pin<Box<dyn Future<Output = Response> + Send>> { ) -> Response {
Box::pin(async move {
info!("📡 Event Subscription request for {}", instance.get_name()); info!("📡 Event Subscription request for {}", instance.get_name());
let method = req.method().as_str(); let method = req.method().as_str();
let sid = headers.get("SID").and_then(|v| v.to_str().ok()).unwrap_or(""); let sid = headers
let timeout = headers.get("Timeout").and_then(|v| v.to_str().ok()).unwrap_or(""); .get("SID")
let callback = headers.get("Callback").and_then(|v| v.to_str().ok()).unwrap_or(""); .and_then(|v| v.to_str().ok())
.unwrap_or("");
let timeout = headers
.get("Timeout")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
let callback = headers
.get("Callback")
.and_then(|v| v.to_str().ok())
.unwrap_or("");
match method { match method {
METHOD_SUBSCRIBE => { METHOD_SUBSCRIBE => {
@@ -584,21 +615,26 @@ fn event_sub_handler(
// Nouvelle souscription // Nouvelle souscription
let new_sid = format!("uuid:{}", uuid::Uuid::new_v4()); let new_sid = format!("uuid:{}", uuid::Uuid::new_v4());
if !callback.is_empty() { if !callback.is_empty() {
instance.add_subscriber(new_sid.clone(), callback.to_string()).await; instance
.add_subscriber(new_sid.clone(), callback.to_string())
.await;
} }
let timeout_val = if timeout.is_empty() { let timeout_val = if timeout.is_empty() {
"Second-1800" "Second-1800"
} else { } else {
timeout timeout
}; };
info!("🔒 New subscription: SID={}, Callback={}, Timeout={}", new_sid, callback, timeout_val); info!(
"🔒 New subscription: SID={}, Callback={}, Timeout={}",
new_sid, callback, timeout_val
);
let sid_clone = new_sid.clone(); let sid_clone = new_sid.clone();
let instance_clone = instance.clone(); let instance_clone = instance.clone();
tokio::spawn(async move { tokio::spawn(async move {
instance_clone.send_initial_event(sid_clone).await; instance_clone.send_initial_event(sid_clone).await;
}); });
(new_sid, timeout_val.to_string()) (new_sid, timeout_val.to_string())
} else { } else {
// Renouvellement // Renouvellement
@@ -611,15 +647,16 @@ fn event_sub_handler(
StatusCode::OK, StatusCode::OK,
[ [
( (
axum::http::header::HeaderName::from_static("sid"), axum::http::header::HeaderName::from_static("sid"),
axum::http::HeaderValue::from_str(&response_sid).unwrap() axum::http::HeaderValue::from_str(&response_sid).unwrap(),
), ),
( (
axum::http::header::HeaderName::from_static("timeout"), axum::http::header::HeaderName::from_static("timeout"),
axum::http::HeaderValue::from_str(&response_timeout).unwrap() axum::http::HeaderValue::from_str(&response_timeout).unwrap(),
), ),
], ],
).into_response() )
.into_response()
} }
METHOD_UNSUBSCRIBE => { METHOD_UNSUBSCRIBE => {
if !sid.is_empty() { if !sid.is_empty() {
@@ -633,19 +670,14 @@ fn event_sub_handler(
StatusCode::METHOD_NOT_ALLOWED.into_response() StatusCode::METHOD_NOT_ALLOWED.into_response()
} }
} }
})
} }
/// Handler Axum pour le contrôle SOAP. /// Handler Axum pour le contrôle SOAP.
fn control_handler( async fn control_handler(State(instance): State<ServiceInstance>, _body: String) -> Response {
State(instance): State<ServiceInstance>,
_body: String,
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
Box::pin(async move {
info!("📡 Control request for {}", instance.get_name()); info!("📡 Control request for {}", instance.get_name());
// TODO: Parser le SOAP et appeler l'action correspondante // TODO: Parser le SOAP et appeler l'action correspondante
let response_xml = format!( let response_xml = format!(
r#"<?xml version="1.0"?> r#"<?xml version="1.0"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
@@ -660,10 +692,13 @@ fn control_handler(
( (
StatusCode::OK, StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "text/xml; charset=\"utf-8\"")], [(
axum::http::header::CONTENT_TYPE,
"text/xml; charset=\"utf-8\"",
)],
response_xml, response_xml,
).into_response() )
}) .into_response()
} }
#[cfg(test)] #[cfg(test)]
@@ -675,7 +710,7 @@ mod tests {
fn test_service_instance_creation() { fn test_service_instance_creation() {
let service = Service::new("AVTransport".to_string()); let service = Service::new("AVTransport".to_string());
let instance = ServiceInstance::new(&service); let instance = ServiceInstance::new(&service);
assert_eq!(instance.get_name(), "AVTransport"); assert_eq!(instance.get_name(), "AVTransport");
assert_eq!(instance.identifier(), "AVTransport"); assert_eq!(instance.identifier(), "AVTransport");
} }
@@ -684,7 +719,7 @@ mod tests {
fn test_service_urls() { fn test_service_urls() {
let service = Service::new("AVTransport".to_string()); let service = Service::new("AVTransport".to_string());
let instance = ServiceInstance::new(&service); let instance = ServiceInstance::new(&service);
assert_eq!(instance.route(), "/service/AVTransport"); assert_eq!(instance.route(), "/service/AVTransport");
assert_eq!(instance.control_route(), "/service/AVTransport/control"); assert_eq!(instance.control_route(), "/service/AVTransport/control");
assert_eq!(instance.event_route(), "/service/AVTransport/event"); assert_eq!(instance.event_route(), "/service/AVTransport/event");
@@ -696,10 +731,10 @@ mod tests {
let mut service = Service::new("AVTransport".to_string()); let mut service = Service::new("AVTransport".to_string());
service.set_version(2).unwrap(); service.set_version(2).unwrap();
let instance = ServiceInstance::new(&service); let instance = ServiceInstance::new(&service);
assert_eq!( assert_eq!(
instance.service_type(), instance.service_type(),
"urn:schemas-upnp-org:service:AVTransport:2" "urn:schemas-upnp-org:service:AVTransport:2"
); );
} }
} }