001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.petstore.console.pets; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.util.*; 022import java.util.concurrent.*; 023import java.util.function.*; 024 025import org.apache.juneau.commons.beanquery.*; 026import org.apache.juneau.http.*; 027import org.apache.juneau.http.response.*; 028import org.apache.juneau.marshall.collections.*; 029import org.apache.juneau.petstore.console.*; 030import org.apache.juneau.petstore.console.pages.*; 031import org.apache.juneau.petstore.service.*; 032import org.apache.juneau.rest.server.*; 033import org.apache.juneau.console.datatables.*; 034import org.apache.juneau.console.datatables.adapter.*; 035import org.apache.juneau.rest.server.view.*; 036import org.apache.juneau.rest.server.view.freemarker.*; 037import org.apache.juneau.console.views.*; 038 039/** 040 * Pending changes: the staged-edits table. Persistent selection is limited to {@code PENDING} rows, an aggregate 041 * <b>Discard</b> returns a {@link BulkResult}, a perRow <b>Apply</b> writes each change to its pet, and a perRow 042 * <b>Retry</b> re-applies a failed change. Also the data source of the header pending-changes badge. 043 * 044 * <h5 class='section'>Example:</h5> 045 * <p class='bcode'> 046 * selection: { rowIdField:'id', selectAll:true, scope:'persistent', labelField:'summary', 047 * selectableWhen:[{field:'state', op:'eq', value:'PENDING', reason:'Only pending changes can be discarded or applied.'}] }, 048 * bulk: { actions: [ 049 * { id:'discard', label:'Discard', bulkMode:'aggregate', endpoint:'/console/pets/changes/discard', method:'POST', tone:'danger' }, 050 * { id:'apply', label:'Apply', bulkMode:'perRow', endpoint:'/console/pets/changes/{id}/apply', method:'POST' } ] } 051 * </p> 052 * <p class='bjava'> 053 * <ja>@RestPost</ja>(path=<js>"/discard"</js>) 054 * <jk>public</jk> BulkResult discard(RestRequest <jv>req</jv>, <ja>@Content</ja> BulkRequest <jv>body</jv>) { 055 * <jk>var</jk> <jv>o</jv> = store().pendingChanges().discard(<jv>body</jv>.<jf>ids</jf>, ConsoleWrites.<jsm>actor</jsm>(<jv>req</jv>)); 056 * <jk>var</jk> <jv>b</jv> = BulkResult.<jsm>create</jsm>().succeeded(<jv>o</jv>.succeeded().toArray(String[]::<jk>new</jk>)).notFound(...); 057 * <jv>o</jv>.failed().forEach(<jv>f</jv> -> <jv>b</jv>.failed(<jk>new</jk> BulkResult.Failure(<jv>f</jv>.id(), <jv>f</jv>.message()))); 058 * <jk>return</jk> <jv>b</jv>.build(); 059 * } 060 * </p> 061 */ 062@Rest(path="/changes", title="Pending changes") 063@SuppressWarnings({ 064 "java:S110" // Inheritance depth comes from the BasicRestServlet hierarchy, not this page. 065}) 066public class ChangesRest extends PetstoreConsolePage { 067 068 private static final long serialVersionUID = 1L; 069 070 /** Replayed discard results by idempotency key (demo-sized; cleared when it grows past the bound). */ 071 private final transient Map<String,BulkResult> replays = new ConcurrentHashMap<>(); 072 073 /** 074 * The Pending changes page. 075 * 076 * @param req The request; {@code ?builder} renders the builder twin. 077 * @return The page. 078 */ 079 @RestGet(path="/") 080 public View page(RestRequest req) { 081 return builder(req) ? twin(req, ChangesPageBuilders.changes()) : FreemarkerView.of("changes.ftlh"); 082 } 083 084 /** 085 * Server-mode DataTables query over staged changes. 086 * 087 * @param req The DataTables request. 088 * @return One page of changes, or a 200 {@code error} envelope for a malformed search expression. 089 */ 090 @RestPost(path="/query") 091 public DataTablesResults<PendingChange> query(@Content DataTablesRequest req) { 092 try (var s = store().queryPendingChanges()) { 093 return DataTablesQuery.run(req, s); 094 } catch (BeanQuerySyntaxException e) { 095 return DataTablesResults.error(req.getDraw(), e.getMessage()); 096 } 097 } 098 099 /** 100 * The header badge source: pending changes, flagged {@code mine} for the caller. 101 * 102 * @param req The request. 103 * @return {@code {total, mine, items:[{id,label,href,mine,kind}]}}. 104 */ 105 @RestGet(path="/pending") 106 public JsonMap pending(RestRequest req) { 107 var me = ConsoleWrites.author(ConsoleWrites.actor(req)); 108 var items = new JsonList(); 109 var mine = 0; 110 for (var c : store().pendingChanges().pending()) { 111 var isMine = eq(c.getAuthor(), me); 112 if (isMine) 113 mine++; 114 items.add(JsonMap.of("id", String.valueOf(c.getId()), "label", c.getSummary(), "href", f("/console/pets/%s/summary", c.getPetId()), "mine", isMine, "kind", "Pet")); 115 } 116 return JsonMap.of("total", items.size(), "mine", mine, "items", items); 117 } 118 119 /** 120 * Applies one change (the bulk perRow action). 121 * 122 * @param req The request. 123 * @param id The change id. 124 * @return {@code success} with the change when applied, {@code failure} with the reason when the write failed. 125 * @throws NotFound If the change does not exist. 126 * @throws Conflict If it is not pending. 127 */ 128 @RestPost(path="/{id}/apply") 129 public ActionResult apply(RestRequest req, @Path("id") long id) { 130 var actor = ConsoleWrites.actor(req); 131 return outcome(call(() -> store().pendingChanges().apply(id, actor))); 132 } 133 134 /** 135 * Retries a failed change. 136 * 137 * @param req The request. 138 * @param id The change id. 139 * @return As {@link #apply(RestRequest, long)}. 140 * @throws NotFound If the change does not exist. 141 * @throws Conflict If it has not failed. 142 */ 143 @RestPost(path="/{id}/retry") 144 public ActionResult retry(RestRequest req, @Path("id") long id) { 145 var actor = ConsoleWrites.actor(req); 146 return outcome(call(() -> store().pendingChanges().retry(id, actor))); 147 } 148 149 /** 150 * Discards changes in one aggregate call. The same key gets the same answer. 151 * 152 * @param req The request. 153 * @param body {@code {ids, idempotencyKey}}. 154 * @param fail {@code discard} to simulate a transport failure (demo toggle). 155 * @return The per-id outcome. 156 * @throws BadRequest If the key or the ids are missing. 157 * @throws ServiceUnavailable On {@code ?fail=discard}. 158 */ 159 @RestPost(path="/discard") 160 public BulkResult discard(RestRequest req, @Content BulkRequest body, @Query("fail") String fail) { 161 var actor = ConsoleWrites.actor(req); 162 if ("discard".equals(fail)) 163 throw new ServiceUnavailable("Discard is unavailable (demo toggle '%s')", fail); 164 if (body.ids == null) 165 throw new BadRequest("Missing 'ids'"); 166 var key = body.idempotencyKey; 167 if (ib(key)) 168 throw new BadRequest("Missing 'idempotencyKey'"); 169 if (replays.size() > 1_000) 170 replays.clear(); 171 return replays.computeIfAbsent(key, k -> { 172 var o = store().pendingChanges().discard(body.ids, actor); 173 var b = BulkResult.create().succeeded(o.succeeded().toArray(String[]::new)).notFound(o.notFound().toArray(String[]::new)); 174 o.failed().forEach(x -> b.failed(new BulkResult.Failure(x.id(), x.message()))); 175 return b.build(); 176 }); 177 } 178 179 private static ActionResult outcome(PendingChange c) { 180 return c.getState() == PendingChange.State.APPLIED 181 ? ActionResult.success(c).message(f("Applied '%s'", c.getSummary())) 182 : ActionResult.failure().row(c).message(c.getMessage()); 183 } 184 185 private static PendingChange call(Supplier<PendingChange> s) { 186 try { 187 return s.get(); 188 } catch (PetstoreNotFoundException e) { 189 throw new NotFound(e.getMessage()); 190 } catch (PetstoreConflictException e) { 191 throw new Conflict(e.getMessage()); 192 } 193 } 194}