From 35d25bbc9d261e7b5585d0fd1d398dff3ab4a176 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Fri, 13 Jan 2023 17:38:06 +0300 Subject: Fix OpenAPI generation * Use HandlerMethodArgumentResolver to pass visitor * Hide visitor from OpenAPI definitions * Drop unused AsciiDoc template --- src/main/java/com/juick/www/api/Index.java | 2 +- src/main/java/com/juick/www/api/Mastodon.java | 11 ++++++----- src/main/java/com/juick/www/api/Messages.java | 13 +++++++------ src/main/java/com/juick/www/api/Notifications.java | 13 +++++++------ src/main/java/com/juick/www/api/PM.java | 7 ++++--- src/main/java/com/juick/www/api/Post.java | 13 +++++++------ src/main/java/com/juick/www/api/Service.java | 7 ++++--- src/main/java/com/juick/www/api/Tags.java | 3 ++- src/main/java/com/juick/www/api/Users.java | 17 +++++++++-------- src/main/java/com/juick/www/api/activity/Profile.java | 7 ++++--- 10 files changed, 51 insertions(+), 42 deletions(-) (limited to 'src/main/java/com/juick/www/api') diff --git a/src/main/java/com/juick/www/api/Index.java b/src/main/java/com/juick/www/api/Index.java index 46aa65b5..921ebca4 100644 --- a/src/main/java/com/juick/www/api/Index.java +++ b/src/main/java/com/juick/www/api/Index.java @@ -45,7 +45,7 @@ public class Index { @Hidden @RequestMapping(value = { "/api/", "/ws/" }, method = RequestMethod.GET) public ResponseEntity description() { - URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri(); + URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().replacePath("/api/swagger-ui/index.html").build().toUri(); return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build(); } diff --git a/src/main/java/com/juick/www/api/Mastodon.java b/src/main/java/com/juick/www/api/Mastodon.java index 5982209e..c595bb19 100644 --- a/src/main/java/com/juick/www/api/Mastodon.java +++ b/src/main/java/com/juick/www/api/Mastodon.java @@ -33,6 +33,7 @@ import com.juick.util.HttpBadRequestException; import com.juick.util.MessageUtils; import com.juick.www.WebApp; import com.juick.www.api.activity.helpers.ProfileUriBuilder; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.Valid; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Value; @@ -166,7 +167,7 @@ public class Mastodon { } @GetMapping("/api/v1/accounts/verify_credentials") - public CredentialAccount account(@ModelAttribute User visitor) { + public CredentialAccount account(@Parameter(hidden = true) User visitor) { return toAccount(visitor); } @@ -220,7 +221,7 @@ public class Mastodon { } @GetMapping("/api/v1/accounts/relationships") - public List relationships(@ModelAttribute User visitor, @RequestParam(value = "id[]") String[] ids) { + public List relationships(@Parameter(hidden = true) User visitor, @RequestParam(value = "id[]") String[] ids) { return Stream.of(ids).map( id -> findRelationships(String.valueOf(visitor.getUid()), id) ).collect(Collectors.toList()); @@ -283,7 +284,7 @@ public class Mastodon { } @GetMapping("/api/v1/conversations") - public List conversations(@ModelAttribute User visitor) { + public List conversations(@Parameter(hidden = true) User visitor) { return chatService.getLastChats(visitor).stream().map( this::toConversation ).collect(Collectors.toList()); @@ -313,7 +314,7 @@ public class Mastodon { } @GetMapping("/api/v1/timelines/{timeline}") - public List publicTimeline(@ModelAttribute User visitor, + public List publicTimeline(@Parameter(hidden = true) User visitor, @PathVariable String timeline, @RequestParam(value = "max_id", required = false) String maxId, @RequestParam(value = "only_media", required = false, defaultValue = "false") Boolean media) { @@ -349,7 +350,7 @@ public class Mastodon { } @GetMapping("/api/v1/statuses/{mid}-{rid}/context") - public Context thread(@ModelAttribute User visitor, @PathVariable int mid, @PathVariable int rid) { + public Context thread(@Parameter(hidden = true) User visitor, @PathVariable int mid, @PathVariable int rid) { var thread = messagesService.getReplies(visitor, mid).stream() .filter(m -> m.getRid() > rid) .peek(msg -> msg.getUser().setAvatar(webApp.getAvatarUrl(msg.getUser()))) diff --git a/src/main/java/com/juick/www/api/Messages.java b/src/main/java/com/juick/www/api/Messages.java index e23356a4..fb10d7a7 100644 --- a/src/main/java/com/juick/www/api/Messages.java +++ b/src/main/java/com/juick/www/api/Messages.java @@ -30,6 +30,7 @@ import com.juick.service.MessagesService; import com.juick.service.TagService; import com.juick.service.UserService; import com.juick.service.component.SystemEvent; +import io.swagger.v3.oas.annotations.Parameter; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; @@ -68,7 +69,7 @@ public class Messages { // TODO: serialize image urls @GetMapping({"/api/home"}) - public List getHome(@ModelAttribute User visitor, + public List getHome(@Parameter(hidden = true) User visitor, @RequestParam(defaultValue = "0") int before_mid) { int vuid = visitor.getUid(); List mids = messagesService.getMyFeed(vuid, before_mid, true); @@ -78,7 +79,7 @@ public class Messages { } @GetMapping("/api/messages") - public List getMessages(@ModelAttribute User visitor, + public List getMessages(@Parameter(hidden = true) User visitor, @RequestParam(required = false) String uname, @RequestParam(name = "before_mid", defaultValue = "0") Integer before, @RequestParam(required = false, defaultValue = "0") Integer daysback, @@ -136,7 +137,7 @@ public class Messages { } @DeleteMapping("/api/messages") - public CommandResult deleteMessage(@ModelAttribute User visitor, @RequestParam int mid, + public CommandResult deleteMessage(@Parameter(hidden = true) User visitor, @RequestParam int mid, @RequestParam(required = false, defaultValue = "0") int rid) { if (rid > 0) { if (messagesService.deleteReply(visitor.getUid(), mid, rid)) { @@ -150,7 +151,7 @@ public class Messages { } @GetMapping("/api/messages/discussions") - public List getDiscussions(@ModelAttribute User visitor, + public List getDiscussions(@Parameter(hidden = true) User visitor, @RequestParam(required = false, defaultValue = "0") Long to) { List msgs = messagesService.getMessages(visitor, messagesService.getDiscussions(visitor.getUid(), to)); msgs.forEach(m -> m.getUser().setAvatar(webApp.getAvatarUrl(m.getUser()))); @@ -158,7 +159,7 @@ public class Messages { } @GetMapping("/api/thread") - public List getThread(@ModelAttribute User visitor, @RequestParam(defaultValue = "0") int mid, + public List getThread(@Parameter(hidden = true) User visitor, @RequestParam(defaultValue = "0") int mid, @RequestParam(defaultValue = "true") boolean showReplies) { Optional message = messagesService.getMessage(mid); if (message.isPresent()) { @@ -188,7 +189,7 @@ public class Messages { } @GetMapping(value = "/api/thread/mark_read/{mid}-{rid}.gif", produces = MediaType.IMAGE_GIF_VALUE) - public byte[] markThreadRead(@ModelAttribute User visitor, @PathVariable int mid, @PathVariable int rid) + public byte[] markThreadRead(@Parameter(hidden = true) User visitor, @PathVariable int mid, @PathVariable int rid) throws IOException { if (!visitor.isAnonymous()) { messagesService.setLastReadComment(visitor, mid, rid); diff --git a/src/main/java/com/juick/www/api/Notifications.java b/src/main/java/com/juick/www/api/Notifications.java index 32ba3dc1..e5e85eb7 100644 --- a/src/main/java/com/juick/www/api/Notifications.java +++ b/src/main/java/com/juick/www/api/Notifications.java @@ -22,6 +22,7 @@ import com.juick.service.*; import com.juick.util.HttpBadRequestException; import com.juick.util.HttpForbiddenException; import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Parameter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; @@ -71,7 +72,7 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGet( - @ModelAttribute(binding = false) User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(required = false, defaultValue = "0") int uid, @RequestParam(required = false, defaultValue = "0") int mid, @RequestParam(required = false, defaultValue = "0") int rid) { @@ -110,7 +111,7 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE) public Status doDelete( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestBody List list) { if (!visitor.equals(serviceUser)) { throw new HttpForbiddenException(); @@ -129,7 +130,7 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications/delete", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public Status doDeleteTokens( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestBody List list) { if (!visitor.equals(serviceUser)) { throw new HttpForbiddenException(); @@ -149,7 +150,7 @@ public class Notifications { @Hidden @RequestMapping(value = "/api/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) public Status doPut( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestBody List list) { list.forEach(t -> { switch (t.type()) { @@ -165,7 +166,7 @@ public class Notifications { @Deprecated @RequestMapping(value = "/api/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public Status doAndroidRegister( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(name = "regid") String regId) { pushQueriesService.addGCMToken(visitor.getUid(), regId); return Status.OK; @@ -174,7 +175,7 @@ public class Notifications { @Deprecated @RequestMapping(value = "/api/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public Status doWinphoneRegister( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(name = "url") String regId) { pushQueriesService.addMPNSToken(visitor.getUid(), regId); return Status.OK; diff --git a/src/main/java/com/juick/www/api/PM.java b/src/main/java/com/juick/www/api/PM.java index c4acd4b3..5f0988c1 100644 --- a/src/main/java/com/juick/www/api/PM.java +++ b/src/main/java/com/juick/www/api/PM.java @@ -29,6 +29,7 @@ import com.juick.www.WebApp; import com.juick.service.ChatService; import com.juick.service.UserService; import com.juick.service.component.SystemEvent; +import io.swagger.v3.oas.annotations.Parameter; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; @@ -53,7 +54,7 @@ public class PM { @RequestMapping(value = "/api/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetPM( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(required = false) String uname) { int uid = 0; if (uname != null && uname.matches("^[a-zA-Z0-9\\-]{2,16}$")) { @@ -71,7 +72,7 @@ public class PM { @RequestMapping(value = "/api/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public Message doPostPM( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam String uname, @RequestParam String body) { User userTo = AnonymousUser.INSTANCE; @@ -103,7 +104,7 @@ public class PM { } @RequestMapping(value = "/api/groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public PrivateChats doGetGroupsPMs( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(defaultValue = "5") int cnt) { // TODO: ignore cnt param for now but make sure paging param will not be cnt diff --git a/src/main/java/com/juick/www/api/Post.java b/src/main/java/com/juick/www/api/Post.java index 2dba6e07..97311c21 100644 --- a/src/main/java/com/juick/www/api/Post.java +++ b/src/main/java/com/juick/www/api/Post.java @@ -25,6 +25,7 @@ import java.util.Optional; import javax.inject.Inject; import com.juick.www.api.activity.helpers.ProfileUriBuilder; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.validation.constraints.NotNull; import com.juick.CommandsManager; @@ -74,7 +75,7 @@ public class Post { @RequestMapping(value = "/api/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) @ResponseStatus(value = HttpStatus.OK) public CommandResult doPostMessage( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body, @RequestParam(required = false) String img, @RequestParam(required = false) MultipartFile attach) throws Exception { @@ -105,7 +106,7 @@ public class Post { @RequestMapping(value = "/api/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public CommandResult doPostComment( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(defaultValue = "0") int mid, @RequestParam(defaultValue = "0") int rid, @RequestParam(required = false, defaultValue = StringUtils.EMPTY) final String body, @@ -157,7 +158,7 @@ public class Post { @PostMapping("/api/like") @ResponseStatus(value = HttpStatus.OK) - public Status doPostRecommendation(@ModelAttribute User visitor, @RequestParam Integer mid) throws Exception { + public Status doPostRecommendation(@Parameter(hidden = true) User visitor, @RequestParam Integer mid) throws Exception { Optional message = messagesService.getMessage(mid); if (message.isEmpty()) { throw new HttpNotFoundException(); @@ -173,7 +174,7 @@ public class Post { @PostMapping("/api/subscribe") @ResponseStatus(value = HttpStatus.OK) - public Status doPostSubscribe(@ModelAttribute User visitor, + public Status doPostSubscribe(@Parameter(hidden = true) User visitor, @RequestParam Integer mid) throws Exception { Optional message = messagesService.getMessage(mid); if (message.isEmpty()) { @@ -197,7 +198,7 @@ public class Post { @PostMapping("/api/react") @ResponseStatus(value = HttpStatus.OK) public Status doPostReact( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam Integer mid, @RequestParam @NotNull int reactionId, @RequestParam(required = false, defaultValue = "1") int count) { @@ -219,7 +220,7 @@ public class Post { } @PostMapping("/api/update") - public CommandResult updateMessage(@ModelAttribute User visitor, + public CommandResult updateMessage(@Parameter(hidden = true) User visitor, @RequestParam Integer mid, @RequestParam(required = false, defaultValue = "0") Integer rid, @RequestParam String body) { diff --git a/src/main/java/com/juick/www/api/Service.java b/src/main/java/com/juick/www/api/Service.java index f4599a56..1e3dcdc8 100644 --- a/src/main/java/com/juick/www/api/Service.java +++ b/src/main/java/com/juick/www/api/Service.java @@ -31,6 +31,7 @@ import com.juick.service.StorageService; import com.juick.service.UserService; import com.juick.service.component.AccountVerificationEvent; import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Parameter; import jakarta.mail.Session; import jakarta.mail.internet.AddressException; import jakarta.mail.internet.InternetAddress; @@ -88,7 +89,7 @@ public class Service { @Hidden @PostMapping("/api/mail") @ResponseStatus(value = HttpStatus.OK) - public void processMail(@ModelAttribute User current, InputStream data) throws Exception { + public void processMail(@Parameter(hidden = true) User current, InputStream data) throws Exception { if (current.equals(serviceUser)) { MimeMessage msg = new MimeMessage(session, data); String[] returnPaths = msg.getHeader("Return-Path"); @@ -203,7 +204,7 @@ public class Service { @Hidden @PostMapping("/api/mail/unsubscribe") @ResponseStatus(value = HttpStatus.OK) - public void processMailUnsubscribe(@ModelAttribute User current, InputStream data) throws Exception { + public void processMailUnsubscribe(@Parameter(hidden = true) User current, InputStream data) throws Exception { if (current.equals(serviceUser)) { MimeMessage msg = new MimeMessage(session, data); String from = msg.getFrom() == null || msg.getFrom().length > 1 @@ -227,7 +228,7 @@ public class Service { } @GetMapping("/api/events") - public SseEmitter handle(@ModelAttribute User visitor) { + public SseEmitter handle(@Parameter(hidden = true) User visitor) { logger.info("{} connected", visitor.getName()); if (!visitor.isAnonymous()) { userService.updateLastSeen(visitor); diff --git a/src/main/java/com/juick/www/api/Tags.java b/src/main/java/com/juick/www/api/Tags.java index 2b6405ac..5bb25af6 100644 --- a/src/main/java/com/juick/www/api/Tags.java +++ b/src/main/java/com/juick/www/api/Tags.java @@ -20,6 +20,7 @@ package com.juick.www.api; import com.juick.model.User; import com.juick.model.TagStats; import com.juick.service.TagService; +import io.swagger.v3.oas.annotations.Parameter; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; @@ -36,7 +37,7 @@ public class Tags { @RequestMapping(value = "/api/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List tags( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(required = false, defaultValue = "0") int user_id ) { if (user_id > 0) { diff --git a/src/main/java/com/juick/www/api/Users.java b/src/main/java/com/juick/www/api/Users.java index 124632d0..afca7ee3 100644 --- a/src/main/java/com/juick/www/api/Users.java +++ b/src/main/java/com/juick/www/api/Users.java @@ -42,6 +42,7 @@ import com.juick.util.HttpUtils; import com.juick.util.WebUtils; import com.juick.www.WebApp; +import io.swagger.v3.oas.annotations.Parameter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -74,7 +75,7 @@ public class Users { @RequestMapping(value = "/api/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetUsers( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam(value = "uname", required = false) List unames) { List users = new ArrayList<>(); @@ -96,7 +97,7 @@ public class Users { } @GetMapping("/api/me") - public SecureUser getMe(@ModelAttribute User visitor) { + public SecureUser getMe(@Parameter(hidden = true) User visitor) { SecureUser me = new SecureUser(); me.setUid(visitor.getUid()); me.setName(visitor.getName()); @@ -115,7 +116,7 @@ public class Users { return (SecureUser)userService.getUserInfo(me); } @PostMapping("/api/me") - public void updateMe(@ModelAttribute User visitor, + public void updateMe(@Parameter(hidden = true) User visitor, @RequestParam(required = false) String password, @RequestParam(value = "jid-del", required = false) String jidForDeletion, @RequestParam(value = "email-add", required = false) String newEmail, @@ -159,12 +160,12 @@ public class Users { } } @PostMapping("/api/me/subscribe") - public void subscribeMe(@ModelAttribute User visitor, String email) { + public void subscribeMe(@Parameter(hidden = true) User visitor, String email) { // TODO: check status emailService.setNotificationsEmail(visitor.getUid(), email); } @PostMapping("/api/me/upload") - public void updateInfo(@ModelAttribute User visitor, + public void updateInfo(@Parameter(hidden = true) User visitor, @RequestParam MultipartFile avatar) throws IOException { String avatarTmpPath = HttpUtils.receiveMultiPartFile(avatar, storageService.getTemporaryDirectory()).getHost(); if (StringUtils.isNotEmpty(avatarTmpPath)) { @@ -175,7 +176,7 @@ public class Users { @RequestMapping(value = "/api/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetUserRead( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam String uname) { int uid = 0; if (uname == null) { @@ -199,7 +200,7 @@ public class Users { @RequestMapping(value = "/api/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List doGetUserReaders( - @ModelAttribute User visitor, + @Parameter(hidden = true) User visitor, @RequestParam String uname) { int uid = 0; if (uname == null) { @@ -222,7 +223,7 @@ public class Users { } @GetMapping("/api/info/{uname}") - public User getUserInfo(@ModelAttribute User visitor, @PathVariable String uname) { + public User getUserInfo(@Parameter(hidden = true) User visitor, @PathVariable String uname) { User user = userService.getUserByName(uname); if (!user.isBanned()) { user.setRead(doGetUserRead(visitor, uname)); diff --git a/src/main/java/com/juick/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java index bf4bda25..19a28a39 100644 --- a/src/main/java/com/juick/www/api/activity/Profile.java +++ b/src/main/java/com/juick/www/api/activity/Profile.java @@ -52,6 +52,7 @@ import com.juick.service.activities.FollowEvent; import com.juick.service.activities.UndoAnnounceEvent; import com.juick.service.activities.UndoFollowEvent; import com.overzealous.remark.Remark; +import io.swagger.v3.oas.annotations.Parameter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -133,8 +134,8 @@ public class Profile { @GetMapping(value = "/u/{userName}/blog", produces = { Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITY_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE, MediaType.APPLICATION_JSON_VALUE }) - public OrderedCollectionPage getOutboxPage(@ModelAttribute User visitor, @PathVariable String userName, - @RequestParam(required = false, defaultValue = "0") int before) { + public OrderedCollectionPage getOutboxPage(@Parameter(hidden = true) User visitor, @PathVariable String userName, + @RequestParam(required = false, defaultValue = "0") int before) { User user = userService.getUserByName(userName); if (!user.isAnonymous() && !user.isBanned()) { UriComponentsBuilder uri = UriComponentsBuilder.fromUriString(baseUri); @@ -278,7 +279,7 @@ public class Profile { @CacheEvict(cacheNames = "profiles", key = "{ #visitor.uri }") @PostMapping(value = "/api/inbox", consumes = { Context.LD_JSON_MEDIA_TYPE, Context.ACTIVITY_MEDIA_TYPE, Context.ACTIVITYSTREAMS_PROFILE_MEDIA_TYPE, MediaType.APPLICATION_JSON_VALUE }) - public ResponseEntity processInbox(@ModelAttribute User visitor, InputStream inboxData) + public ResponseEntity processInbox(@Parameter(hidden = true) User visitor, InputStream inboxData) throws Exception { String inbox = IOUtils.toString(inboxData, StandardCharsets.UTF_8); Activity activity = jsonMapper.readValue(inbox, Activity.class); -- cgit v1.2.3