2017-03-27 08:53:45 +00:00
|
|
|
use std::ops::Deref;
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::fmt;
|
|
|
|
|
2017-03-29 11:08:53 +00:00
|
|
|
use smallvec::SmallVec;
|
|
|
|
|
2017-04-20 20:43:01 +00:00
|
|
|
use ext::IntoCollection;
|
|
|
|
use http::{Header, MediaType};
|
2017-03-29 11:08:53 +00:00
|
|
|
use http::parse::parse_accept;
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
/// A `MediaType` with an associated quality value.
|
2017-03-29 11:08:53 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
2017-06-22 11:29:59 +00:00
|
|
|
pub struct QMediaType(pub MediaType, pub Option<f32>);
|
2017-03-27 08:53:45 +00:00
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
impl QMediaType {
|
|
|
|
/// Retrieve the weight of the media type, if there is any.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{MediaType, QMediaType};
|
|
|
|
///
|
|
|
|
/// let q_type = QMediaType(MediaType::HTML, Some(0.3));
|
|
|
|
/// assert_eq!(q_type.weight(), Some(0.3));
|
|
|
|
/// ```
|
2017-03-27 08:53:45 +00:00
|
|
|
#[inline(always)]
|
|
|
|
pub fn weight(&self) -> Option<f32> {
|
|
|
|
self.1
|
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
/// Retrieve the weight of the media type or a given default value.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{MediaType, QMediaType};
|
|
|
|
///
|
|
|
|
/// let q_type = QMediaType(MediaType::HTML, Some(0.3));
|
|
|
|
/// assert_eq!(q_type.weight_or(0.9), 0.3);
|
|
|
|
///
|
|
|
|
/// let q_type = QMediaType(MediaType::HTML, None);
|
|
|
|
/// assert_eq!(q_type.weight_or(0.9), 0.9);
|
|
|
|
/// ```
|
2017-03-27 08:53:45 +00:00
|
|
|
#[inline(always)]
|
|
|
|
pub fn weight_or(&self, default: f32) -> f32 {
|
|
|
|
self.1.unwrap_or(default)
|
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
/// Borrow the internal `MediaType`.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{MediaType, QMediaType};
|
|
|
|
///
|
|
|
|
/// let q_type = QMediaType(MediaType::HTML, Some(0.3));
|
|
|
|
/// assert_eq!(q_type.media_type(), &MediaType::HTML);
|
|
|
|
/// ```
|
2017-03-27 08:53:45 +00:00
|
|
|
#[inline(always)]
|
2017-03-29 11:08:53 +00:00
|
|
|
pub fn media_type(&self) -> &MediaType {
|
|
|
|
&self.0
|
|
|
|
}
|
2017-03-27 08:53:45 +00:00
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
impl From<MediaType> for QMediaType {
|
2017-03-29 11:08:53 +00:00
|
|
|
#[inline(always)]
|
2017-06-22 11:29:59 +00:00
|
|
|
fn from(media_type: MediaType) -> QMediaType {
|
|
|
|
QMediaType(media_type, None)
|
2017-03-29 11:08:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
impl Deref for QMediaType {
|
2017-03-27 08:53:45 +00:00
|
|
|
type Target = MediaType;
|
|
|
|
|
|
|
|
#[inline(always)]
|
|
|
|
fn deref(&self) -> &MediaType {
|
|
|
|
&self.0
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-29 11:08:53 +00:00
|
|
|
// FIXME: `Static` is needed for `const` items. Need `const SmallVec::new`.
|
2017-06-25 04:00:50 +00:00
|
|
|
#[derive(Debug, Clone)]
|
2017-03-29 11:08:53 +00:00
|
|
|
pub enum AcceptParams {
|
2017-06-22 11:29:59 +00:00
|
|
|
Static(&'static [QMediaType]),
|
|
|
|
Dynamic(SmallVec<[QMediaType; 1]>)
|
2017-03-29 11:08:53 +00:00
|
|
|
}
|
|
|
|
|
2017-06-25 04:00:50 +00:00
|
|
|
impl PartialEq for AcceptParams {
|
|
|
|
fn eq(&self, other: &AcceptParams) -> bool {
|
|
|
|
#[inline(always)]
|
|
|
|
fn inner_types(params: &AcceptParams) -> &[QMediaType] {
|
|
|
|
match *params {
|
|
|
|
AcceptParams::Static(params) => params,
|
|
|
|
AcceptParams::Dynamic(ref vec) => vec,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
inner_types(self) == inner_types(other)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-27 08:53:45 +00:00
|
|
|
/// The HTTP Accept header.
|
2017-06-22 11:29:59 +00:00
|
|
|
///
|
|
|
|
/// An `Accept` header is composed of zero or more media types, each of which
|
|
|
|
/// may have an optional quality value (a [`QMediaType`]). The header is sent by an HTTP client to
|
|
|
|
/// describe the formats it accepts as well as the order in which it prefers
|
|
|
|
/// different formats.
|
|
|
|
///
|
|
|
|
/// # Usage
|
|
|
|
///
|
|
|
|
/// The Accept header of an incoming request can be retrieved via the
|
|
|
|
/// [`Request::accept`] method. The [`preferred`] method can be used to retrieve
|
|
|
|
/// the client's preferred media type.
|
|
|
|
///
|
|
|
|
/// [`Request::accept`]: /rocket/struct.Request.html#method.accept
|
|
|
|
/// [`preferred`]: /rocket/http/struct.Accept.html#method.preferred
|
|
|
|
/// [`QMediaType`]: /rocket/http/struct.QMediaType.html
|
|
|
|
///
|
|
|
|
/// An `Accept` type with a single, common media type can be easily constructed
|
|
|
|
/// via provided associated constants.
|
|
|
|
///
|
|
|
|
/// ## Example
|
|
|
|
///
|
|
|
|
/// Construct an `Accept` header with a single `application/json` media type:
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::Accept;
|
|
|
|
///
|
|
|
|
/// # #[allow(unused_variables)]
|
|
|
|
/// let accept_json = Accept::JSON;
|
|
|
|
/// ```
|
|
|
|
///
|
|
|
|
/// # Header
|
|
|
|
///
|
|
|
|
/// `Accept` implements `Into<Header>`. As such, it can be used in any context
|
|
|
|
/// where an `Into<Header>` is expected:
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::Accept;
|
|
|
|
/// use rocket::response::Response;
|
|
|
|
///
|
|
|
|
/// # #[allow(unused_variables)]
|
|
|
|
/// let response = Response::build().header(Accept::JSON).finalize();
|
|
|
|
/// ```
|
2017-03-29 11:08:53 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
|
|
pub struct Accept(AcceptParams);
|
|
|
|
|
|
|
|
macro_rules! accept_constructor {
|
|
|
|
($($name:ident ($check:ident): $str:expr, $t:expr,
|
2018-01-30 20:30:31 +00:00
|
|
|
$s:expr $(; $k:expr => $v:expr)*,)+) => {
|
2017-03-29 11:08:53 +00:00
|
|
|
$(
|
|
|
|
#[doc="An `Accept` header with the single media type for <b>"]
|
|
|
|
#[doc=$str] #[doc="</b>: <i>"]
|
|
|
|
#[doc=$t] #[doc="/"] #[doc=$s]
|
|
|
|
#[doc="</i>"]
|
|
|
|
#[allow(non_upper_case_globals)]
|
|
|
|
pub const $name: Accept = Accept(
|
2017-06-22 11:29:59 +00:00
|
|
|
AcceptParams::Static(&[QMediaType(MediaType::$name, None)])
|
2017-03-29 11:08:53 +00:00
|
|
|
);
|
|
|
|
)+
|
|
|
|
};
|
|
|
|
}
|
2017-03-27 08:53:45 +00:00
|
|
|
|
2017-03-29 11:08:53 +00:00
|
|
|
impl<T: IntoCollection<MediaType>> From<T> for Accept {
|
|
|
|
#[inline(always)]
|
|
|
|
fn from(items: T) -> Accept {
|
|
|
|
Accept(AcceptParams::Dynamic(items.mapped(|item| item.into())))
|
|
|
|
}
|
|
|
|
}
|
2017-03-27 08:53:45 +00:00
|
|
|
|
|
|
|
impl Accept {
|
2017-06-22 11:29:59 +00:00
|
|
|
/// Constructs a new `Accept` header from one or more media types.
|
|
|
|
///
|
|
|
|
/// The `items` parameter may be of type `QMediaType`, `&[QMediaType]`, or
|
|
|
|
/// `Vec<QMediaType>`. To prevent additional allocations, prefer to provide
|
|
|
|
/// inputs of type `QMediaType` and `Vec<QMediaType>`.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{QMediaType, MediaType, Accept};
|
|
|
|
///
|
|
|
|
/// // Construct an `Accept` via a `Vec<QMediaType>`.
|
|
|
|
/// let json_then_html = vec![MediaType::JSON.into(), MediaType::HTML.into()];
|
|
|
|
/// let accept = Accept::new(json_then_html);
|
|
|
|
/// assert_eq!(accept.preferred().media_type(), &MediaType::JSON);
|
|
|
|
///
|
|
|
|
/// // Construct an `Accept` via an `&[QMediaType]`.
|
|
|
|
/// let accept = Accept::new(&[MediaType::JSON.into(), MediaType::HTML.into()]);
|
|
|
|
/// assert_eq!(accept.preferred().media_type(), &MediaType::JSON);
|
|
|
|
///
|
|
|
|
/// // Construct an `Accept` via a `QMediaType`.
|
|
|
|
/// let accept = Accept::new(QMediaType(MediaType::JSON, None));
|
|
|
|
/// assert_eq!(accept.preferred().media_type(), &MediaType::JSON);
|
|
|
|
/// ```
|
2017-03-29 11:08:53 +00:00
|
|
|
#[inline(always)]
|
2017-06-22 11:29:59 +00:00
|
|
|
pub fn new<T: IntoCollection<QMediaType>>(items: T) -> Accept {
|
2017-03-29 11:08:53 +00:00
|
|
|
Accept(AcceptParams::Dynamic(items.into_collection()))
|
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
// TODO: Implement this.
|
2017-03-29 11:08:53 +00:00
|
|
|
// #[inline(always)]
|
2017-06-22 11:29:59 +00:00
|
|
|
// pub fn add<M: Into<QMediaType>>(&mut self, media_type: M) {
|
2017-03-29 11:08:53 +00:00
|
|
|
// self.0.push(media_type.into());
|
|
|
|
// }
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
/// Retrieve the client's preferred media type. This method follows [RFC
|
|
|
|
/// 7231 5.3.2]. If the list of media types is empty, this method returns a
|
|
|
|
/// media type of any with no quality value: (`*/*`).
|
|
|
|
///
|
|
|
|
/// [RFC 7231 5.3.2]: https://tools.ietf.org/html/rfc7231#section-5.3.2
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{QMediaType, MediaType, Accept};
|
|
|
|
///
|
|
|
|
/// let media_types = vec![
|
|
|
|
/// QMediaType(MediaType::JSON, Some(0.3)),
|
|
|
|
/// QMediaType(MediaType::HTML, Some(0.9))
|
|
|
|
/// ];
|
|
|
|
///
|
|
|
|
/// let accept = Accept::new(media_types);
|
|
|
|
/// assert_eq!(accept.preferred().media_type(), &MediaType::HTML);
|
|
|
|
/// ```
|
|
|
|
pub fn preferred(&self) -> &QMediaType {
|
|
|
|
static ANY: QMediaType = QMediaType(MediaType::Any, None);
|
2017-03-30 01:18:30 +00:00
|
|
|
|
2017-03-27 08:53:45 +00:00
|
|
|
// See https://tools.ietf.org/html/rfc7231#section-5.3.2.
|
|
|
|
let mut all = self.iter();
|
|
|
|
let mut preferred = all.next().unwrap_or(&ANY);
|
2017-03-30 01:18:30 +00:00
|
|
|
for media_type in all {
|
|
|
|
if media_type.weight().is_none() && preferred.weight().is_some() {
|
|
|
|
// Media types without a `q` parameter are preferred.
|
|
|
|
preferred = media_type;
|
|
|
|
} else if media_type.weight_or(0.0) > preferred.weight_or(1.0) {
|
|
|
|
// Prefer media types with a greater weight, but if one doesn't
|
|
|
|
// have a weight, prefer the one we already have.
|
|
|
|
preferred = media_type;
|
|
|
|
} else if media_type.specificity() > preferred.specificity() {
|
|
|
|
// Prefer more specific media types over less specific ones. IE:
|
|
|
|
// text/html over application/*.
|
|
|
|
preferred = media_type;
|
|
|
|
} else if media_type == preferred {
|
|
|
|
// Finally, all other things being equal, prefer a media type
|
|
|
|
// with more parameters over one with fewer. IE: text/html; a=b
|
|
|
|
// over text/html.
|
|
|
|
if media_type.params().count() > preferred.params().count() {
|
|
|
|
preferred = media_type;
|
2017-03-27 08:53:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
preferred
|
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
/// Retrieve the first media type in `self`, if any.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{QMediaType, MediaType, Accept};
|
|
|
|
///
|
|
|
|
/// let accept = Accept::new(QMediaType(MediaType::XML, None));
|
|
|
|
/// assert_eq!(accept.first(), Some(&MediaType::XML.into()));
|
|
|
|
/// ```
|
2017-03-27 08:53:45 +00:00
|
|
|
#[inline(always)]
|
2017-06-22 11:29:59 +00:00
|
|
|
pub fn first(&self) -> Option<&QMediaType> {
|
2017-03-29 11:08:53 +00:00
|
|
|
self.iter().next()
|
2017-03-27 08:53:45 +00:00
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
/// Returns an iterator over all of the (quality) media types in `self`.
|
|
|
|
/// Media types are returned in the order in which they appear in the
|
|
|
|
/// header.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{QMediaType, MediaType, Accept};
|
|
|
|
///
|
|
|
|
/// let qmedia_types = vec![
|
|
|
|
/// QMediaType(MediaType::JSON, Some(0.3)),
|
|
|
|
/// QMediaType(MediaType::HTML, Some(0.9))
|
|
|
|
/// ];
|
|
|
|
///
|
|
|
|
/// let accept = Accept::new(qmedia_types.clone());
|
|
|
|
///
|
|
|
|
/// let mut iter = accept.iter();
|
|
|
|
/// assert_eq!(iter.next(), Some(&qmedia_types[0]));
|
|
|
|
/// assert_eq!(iter.next(), Some(&qmedia_types[1]));
|
|
|
|
/// assert_eq!(iter.next(), None);
|
|
|
|
/// ```
|
2017-03-27 08:53:45 +00:00
|
|
|
#[inline(always)]
|
2017-06-22 11:29:59 +00:00
|
|
|
pub fn iter<'a>(&'a self) -> impl Iterator<Item=&'a QMediaType> + 'a {
|
2017-03-29 11:08:53 +00:00
|
|
|
let slice = match self.0 {
|
|
|
|
AcceptParams::Static(slice) => slice,
|
|
|
|
AcceptParams::Dynamic(ref vec) => &vec[..],
|
|
|
|
};
|
|
|
|
|
|
|
|
slice.iter()
|
2017-03-27 08:53:45 +00:00
|
|
|
}
|
|
|
|
|
2017-06-22 11:29:59 +00:00
|
|
|
/// Returns an iterator over all of the (bare) media types in `self`. Media
|
|
|
|
/// types are returned in the order in which they appear in the header.
|
|
|
|
///
|
|
|
|
/// # Example
|
|
|
|
///
|
|
|
|
/// ```rust
|
|
|
|
/// use rocket::http::{QMediaType, MediaType, Accept};
|
|
|
|
///
|
|
|
|
/// let qmedia_types = vec![
|
|
|
|
/// QMediaType(MediaType::JSON, Some(0.3)),
|
|
|
|
/// QMediaType(MediaType::HTML, Some(0.9))
|
|
|
|
/// ];
|
|
|
|
///
|
|
|
|
/// let accept = Accept::new(qmedia_types.clone());
|
|
|
|
///
|
|
|
|
/// let mut iter = accept.media_types();
|
|
|
|
/// assert_eq!(iter.next(), Some(qmedia_types[0].media_type()));
|
|
|
|
/// assert_eq!(iter.next(), Some(qmedia_types[1].media_type()));
|
|
|
|
/// assert_eq!(iter.next(), None);
|
|
|
|
/// ```
|
2017-03-27 08:53:45 +00:00
|
|
|
#[inline(always)]
|
|
|
|
pub fn media_types<'a>(&'a self) -> impl Iterator<Item=&'a MediaType> + 'a {
|
2017-03-29 11:08:53 +00:00
|
|
|
self.iter().map(|weighted_mt| weighted_mt.media_type())
|
2017-03-27 08:53:45 +00:00
|
|
|
}
|
2017-03-29 11:08:53 +00:00
|
|
|
|
|
|
|
known_media_types!(accept_constructor);
|
2017-03-27 08:53:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl fmt::Display for Accept {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
for (i, media_type) in self.iter().enumerate() {
|
2017-06-22 11:29:59 +00:00
|
|
|
if i >= 1 {
|
|
|
|
write!(f, ", {}", media_type.0)?;
|
|
|
|
} else {
|
|
|
|
write!(f, "{}", media_type.0)?;
|
|
|
|
}
|
2017-03-27 08:53:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Accept {
|
|
|
|
// Ideally we'd return a `ParseError`, but that requires a lifetime.
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn from_str(raw: &str) -> Result<Accept, String> {
|
|
|
|
parse_accept(raw).map_err(|e| e.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-29 11:08:53 +00:00
|
|
|
/// Creates a new `Header` with name `Accept` and the value set to the HTTP
|
|
|
|
/// rendering of this `Accept` header.
|
|
|
|
impl Into<Header<'static>> for Accept {
|
|
|
|
#[inline(always)]
|
|
|
|
fn into(self) -> Header<'static> {
|
|
|
|
Header::new("Accept", self.to_string())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-27 08:53:45 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use http::{Accept, MediaType};
|
|
|
|
|
|
|
|
macro_rules! assert_preference {
|
|
|
|
($string:expr, $expect:expr) => (
|
|
|
|
let accept: Accept = $string.parse().expect("accept string parse");
|
|
|
|
let expected: MediaType = $expect.parse().expect("media type parse");
|
|
|
|
let preferred = accept.preferred();
|
|
|
|
assert_eq!(preferred.media_type().to_string(), expected.to_string());
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_preferred() {
|
|
|
|
assert_preference!("text/*", "text/*");
|
2017-03-30 01:18:30 +00:00
|
|
|
assert_preference!("text/*, text/html", "text/html");
|
2017-03-27 08:53:45 +00:00
|
|
|
assert_preference!("text/*; q=0.1, text/html", "text/html");
|
|
|
|
assert_preference!("text/*; q=1, text/html", "text/html");
|
|
|
|
assert_preference!("text/html, text/*", "text/html");
|
2017-03-30 01:18:30 +00:00
|
|
|
assert_preference!("text/*, text/html", "text/html");
|
2017-03-27 08:53:45 +00:00
|
|
|
assert_preference!("text/html, text/*; q=1", "text/html");
|
2017-03-30 01:18:30 +00:00
|
|
|
assert_preference!("text/html; q=1, text/html", "text/html");
|
2017-03-27 08:53:45 +00:00
|
|
|
assert_preference!("text/html, text/*; q=0.1", "text/html");
|
2017-03-30 01:18:30 +00:00
|
|
|
|
2017-03-27 08:53:45 +00:00
|
|
|
assert_preference!("text/html, application/json", "text/html");
|
2017-03-30 01:18:30 +00:00
|
|
|
assert_preference!("text/html, application/json; q=1", "text/html");
|
|
|
|
assert_preference!("application/json; q=1, text/html", "text/html");
|
|
|
|
|
|
|
|
assert_preference!("text/*, application/json", "application/json");
|
|
|
|
assert_preference!("*/*, text/*", "text/*");
|
|
|
|
assert_preference!("*/*, text/*, text/plain", "text/plain");
|
2017-03-27 08:53:45 +00:00
|
|
|
|
|
|
|
assert_preference!("a/b; q=0.1, a/b; q=0.2", "a/b; q=0.2");
|
|
|
|
assert_preference!("a/b; q=0.1, b/c; q=0.2", "b/c; q=0.2");
|
|
|
|
assert_preference!("a/b; q=0.5, b/c; q=0.2", "a/b; q=0.5");
|
|
|
|
|
|
|
|
assert_preference!("a/b; q=0.5, b/c; q=0.2, c/d", "c/d");
|
|
|
|
assert_preference!("a/b; q=0.5; v=1, a/b", "a/b");
|
|
|
|
|
|
|
|
assert_preference!("a/b; v=1, a/b; v=1; c=2", "a/b; v=1; c=2");
|
|
|
|
assert_preference!("a/b; v=1; c=2, a/b; v=1", "a/b; v=1; c=2");
|
|
|
|
assert_preference!("a/b; q=0.5; v=1, a/b; q=0.5; v=1; c=2",
|
|
|
|
"a/b; q=0.5; v=1; c=2");
|
|
|
|
assert_preference!("a/b; q=0.6; v=1, a/b; q=0.5; v=1; c=2",
|
|
|
|
"a/b; q=0.6; v=1");
|
|
|
|
}
|
|
|
|
}
|