From ba262a5e8ce59bcf661a40ce33bba60e4a0a2400 Mon Sep 17 00:00:00 2001 From: Administrator <zhubaomin> Date: 星期一, 20 五月 2024 11:12:38 +0800 Subject: [PATCH] 2024-05-20 朱宝民 远程模块测试 --- pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/resources/application.yml | 2 pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/PipIrrSellApplication.java | 2 pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/result/RemoteResultCode.java | 24 +++ pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/config/RestTemplateConfig.java | 6 pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/ValveCtrl.java | 108 +++++++++++++ pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/dto/DTOValve.java | 48 ++++++ pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/utils/RestTemplateUtils.java | 97 ++++++++++++ pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebListenerConfiguration.java | 68 ++++++++ pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/RestTemplateConfig.java | 21 ++ pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebFilterConfiguration.java | 51 ++++++ 10 files changed, 424 insertions(+), 3 deletions(-) diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/RestTemplateConfig.java b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/RestTemplateConfig.java new file mode 100644 index 0000000..ede1522 --- /dev/null +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/RestTemplateConfig.java @@ -0,0 +1,21 @@ +package com.dy.pipIrrRemote.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * @author ZhuBaoMin + * @date 2024-05-07 17:09 + * @LastEditTime 2024-05-07 17:09 + * @Description + */ +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + +} diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebFilterConfiguration.java b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebFilterConfiguration.java new file mode 100644 index 0000000..e0949b3 --- /dev/null +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebFilterConfiguration.java @@ -0,0 +1,51 @@ +package com.dy.pipIrrRemote.config; + +import com.dy.common.webFilter.DevOfDataSourceNameSetFilter; +import com.dy.common.webFilter.UserTokenFilter; +import jakarta.servlet.Filter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author ZhuBaoMin + * @date 2024-05-07 14:51 + * @LastEditTime 2024-05-07 14:51 + * @Description + */ +@Configuration +public class WebFilterConfiguration { + + @Value("${pipIrr.global.dev}") + public String isDevStage ;//鏄惁涓哄紑鍙戦樁娈� + @Value("${pipIrr.global.dsName}") + public String dsName ;//寮�鍙戦樁娈电殑鏁版嵁婧愬悕绉� + + /** + * DevOfDataSourceNameSetFilter涓嶶serTokenFilter鍙兘涓�涓閰嶇疆涓婏紝 + * 鎵�浠ヤ粬浠殑order閮芥槸1 + */ + private static final int order_UserTokenFilter = 1 ;//涓庝笅闈� + private static final int order_DevOfDataSourceNameSetFilter = 1 ; + + + @Bean + public FilterRegistrationBean<? extends Filter> RegFilter() { + FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>(); + if(this.isDevStage != null && !this.isDevStage.trim().equals("") && this.isDevStage.trim().equalsIgnoreCase("true")){ + filterRegistrationBean.setFilter(new DevOfDataSourceNameSetFilter()); + filterRegistrationBean.addUrlPatterns("/*");//閰嶇疆杩囨护瑙勫垯 + filterRegistrationBean.addInitParameter("dataSourceName",dsName);//璁剧疆init鍙傛暟 + filterRegistrationBean.setName("DevOfDataSourceNameSetFilter");//璁剧疆杩囨护鍣ㄥ悕绉� + filterRegistrationBean.setOrder(order_DevOfDataSourceNameSetFilter);//鎵ц娆″簭 + }else{ + filterRegistrationBean.setFilter(new UserTokenFilter()); + filterRegistrationBean.addUrlPatterns("/*");//閰嶇疆杩囨护瑙勫垯 + filterRegistrationBean.setName("UserTokenFilter");//璁剧疆杩囨护鍣ㄥ悕绉� + filterRegistrationBean.setOrder(order_UserTokenFilter);//鎵ц娆″簭 + } + return filterRegistrationBean; + } + +} \ No newline at end of file diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebListenerConfiguration.java b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebListenerConfiguration.java new file mode 100644 index 0000000..3c04122 --- /dev/null +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/config/WebListenerConfiguration.java @@ -0,0 +1,68 @@ +package com.dy.pipIrrRemote.config; + +import com.dy.common.webListener.GenerateIdSetSuffixListener; +import jakarta.servlet.ServletContextListener; +import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author ZhuBaoMin + * @date 2024-05-07 14:52 + * @LastEditTime 2024-05-07 14:52 + * @Description + */ +@Configuration +public class WebListenerConfiguration { + + /** + * 鍚姩椤哄簭 + */ + //private static final int order_config = 0 ; + private static final int order_idSetSuffix = 1 ; + //private static final int order_init = 2 ; + + /* + * 瑙f瀽鍚勭***.config閰嶇疆鐨凜onfigListener锛屾殏鏃朵笉閲囩敤姝ょ閰嶇疆鏂瑰紡 + * + @Bean + public ConfigListener getGlConfigListener(){ + return new ConfigListener() ; + } + /** + * 澶栭儴鎻愪緵Listener + * @param listener 澶栭儴鎻愪緵Listener + * @return 娉ㄥ唽Bean + @Bean + public ServletListenerRegistrationBean<? extends ServletContextListener> regConfigListener(ConfigListener listener) { + ServletListenerRegistrationBean<ConfigListener> listenerRegistrationBean = new ServletListenerRegistrationBean<>(); + listenerRegistrationBean.setListener(listener); + listenerRegistrationBean.setOrder(order_config); + return listenerRegistrationBean; + } + */ + + /** + * 鍐呴儴鎻愪緵listener锛岃listener鍦ㄧ郴缁熷惎鍔ㄦ椂锛屾牴鎹厤缃� 璁剧疆ID浜х敓鍣ㄧ殑鍚庣紑 + * @return 娉ㄥ唽Bean + */ + @Bean + public ServletListenerRegistrationBean<? extends ServletContextListener> regSsoListener() { + ServletListenerRegistrationBean<GenerateIdSetSuffixListener> listenerRegistrationBean = new ServletListenerRegistrationBean<>(); + listenerRegistrationBean.setListener(new GenerateIdSetSuffixListener()); + listenerRegistrationBean.setOrder(order_idSetSuffix); + return listenerRegistrationBean; + } + +// /** +// * 鍐呴儴鎻愪緵listener锛岃listener鍦ㄧ郴缁熷惎鍔ㄦ椂锛屽垵濮嬪寲鏁版嵁搴撴暟鎹� +// * @return 娉ㄥ唽Bean +// */ +// @Bean +// public ServletListenerRegistrationBean<? extends ServletContextListener> regInitListener() { +// ServletListenerRegistrationBean<InitListener> listenerRegistrationBean = new ServletListenerRegistrationBean<>(); +// listenerRegistrationBean.setListener(new InitListener()); +// listenerRegistrationBean.setOrder(order_init); +// return listenerRegistrationBean; +// } +} diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/result/RemoteResultCode.java b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/result/RemoteResultCode.java new file mode 100644 index 0000000..1db000d --- /dev/null +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/result/RemoteResultCode.java @@ -0,0 +1,24 @@ +package com.dy.pipIrrRemote.result; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * @author ZhuBaoMin + * @date 2024-05-07 14:54 + * @LastEditTime 2024-05-07 14:54 + * @Description + */ +@Getter +@AllArgsConstructor +public enum RemoteResultCode { + /** + * 杩滅▼鎿嶄綔 + */ + DIVIDE_FAIL(10001, "鍒嗘按鎴挎坊鍔犲け璐�"), + DELETE_DIVIDE_FAIL(10001, "鍒嗘按鎴垮垹闄ゅけ璐�"), + NO_DIVIDES(10001, "鏃犵鍚堟潯浠剁殑鍒嗘按鎴胯褰�"); + + private final Integer code; + private final String message; +} diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/utils/RestTemplateUtils.java b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/utils/RestTemplateUtils.java new file mode 100644 index 0000000..31deb69 --- /dev/null +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/utils/RestTemplateUtils.java @@ -0,0 +1,97 @@ +package com.dy.pipIrrRemote.utils; + +import com.alibaba.fastjson2.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author ZhuBaoMin + * @date 2024-05-07 17:07 + * @LastEditTime 2024-05-07 17:07 + * @Description + */ +@Component +public class RestTemplateUtils { + + @Autowired + private RestTemplate restTemplate; + + public JSONObject get(String url, Map<String, Object> queryParams) throws IOException { + return get(url, queryParams, new HashMap<>(1)); + } + + public JSONObject get(String url, Map<String, Object> queryParams, Map<String, String> headerParams) throws IOException { + String tempUrl = setParamsByAppendUrl(queryParams, url); + HttpHeaders headers = new HttpHeaders(); + headerParams.forEach(headers::add); + HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(null, headers); + ResponseEntity<String> response = restTemplate.exchange(tempUrl, HttpMethod.GET, httpEntity, String.class); + return JSONObject.parseObject(response.getBody()); + } + + public JSONObject get2(String url, Map<String, Object> queryParams, Map<String, String> headerParams) throws IOException { + String tempUrl = setParamsByPath(queryParams, url); + HttpHeaders headers = new HttpHeaders(); + headerParams.forEach(headers::add); + HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(null, headers); + ResponseEntity<String> response = restTemplate.exchange(tempUrl, HttpMethod.GET, httpEntity, String.class, queryParams); + return JSONObject.parseObject(response.getBody()); + } + + public JSONObject post(String url, String json, Map<String, String> headerParams) { + HttpHeaders headers = new HttpHeaders(); + headerParams.forEach(headers::add); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("Accept", MediaType.APPLICATION_JSON.toString()); + HttpEntity<String> httpEntity = new HttpEntity<>(json, headers); + ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class); + return JSONObject.parseObject(response.getBody()); + } + + private String setParamsByPath(Map<String, Object> queryParams, String url) { + // url?id={id}&name={name} + if (queryParams == null || queryParams.isEmpty()) { + return url; + } + StringBuilder sb = new StringBuilder(); + try { + for (Map.Entry<String, Object> entry : queryParams.entrySet()) { + sb.append("&").append(entry.getKey()).append("=").append("{").append(entry.getKey()).append("}"); + } + if (!url.contains("?")) { + sb.deleteCharAt(0).insert(0, "?"); + } + } catch (Exception e) { + e.printStackTrace(); + } + return url + sb; + } + + private String setParamsByAppendUrl(Map<String, Object> queryParams, String url) { + // url?id=1&name=zzc + if (queryParams == null || queryParams.isEmpty()) { + return url; + } + StringBuilder sb = new StringBuilder(); + try { + for (Map.Entry<String, Object> entry : queryParams.entrySet()) { + sb.append("&").append(entry.getKey()).append("="); + sb.append(entry.getValue()); + } + if (!url.contains("?")) { + sb.deleteCharAt(0).insert(0, "?"); + } + } catch (Exception e) { + e.printStackTrace(); + } + return url + sb; + } + +} diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/ValveCtrl.java b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/ValveCtrl.java new file mode 100644 index 0000000..e986f2e --- /dev/null +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/ValveCtrl.java @@ -0,0 +1,108 @@ +package com.dy.pipIrrRemote.valve; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.dy.common.aop.SsoAop; +import com.dy.common.webUtil.BaseResponse; +import com.dy.common.webUtil.BaseResponseUtils; +import com.dy.common.webUtil.ResultCodeMsg; +import com.dy.pipIrrRemote.utils.RestTemplateUtils; +import com.dy.pipIrrRemote.valve.dto.DTOValve; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * @author ZhuBaoMin + * @date 2024-05-07 14:59 + * @LastEditTime 2024-05-07 14:59 + * @Description + */ + +@Slf4j +@Tag(name = "鍒嗘按鎴跨鐞�", description = "鍒嗘按鎴挎搷浣�") +@RestController +@RequestMapping(path="valve") +@RequiredArgsConstructor +public class ValveCtrl { + private final RestTemplateUtils restTemplateUtils; + + private CompletableFuture<String> futureValue = new CompletableFuture<>(); + + /** + * 杩滅▼寮�鍏抽榾 + * @param po 寮�鍏抽榾浼犲叆瀵硅薄 + * @param bindingResult + * @return + */ + @Operation(summary = "杩滅▼寮�鍏抽榾", description = "杩滅▼寮�鍏抽榾") + @ApiResponses(value = { + @ApiResponse( + responseCode = ResultCodeMsg.RsCode.SUCCESS_CODE, + description = "鎿嶄綔缁撴灉锛歵rue锛氭垚鍔燂紝false锛氬け璐ワ紙BaseResponse.content锛�", + content = {@Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + schema = @Schema(implementation = Boolean.class))} + ) + }) + @PostMapping(path = "operate", consumes = MediaType.APPLICATION_JSON_VALUE) + @Transactional(rollbackFor = Exception.class) + @SsoAop() + public BaseResponse<Boolean> open(@RequestBody @Valid DTOValve po, BindingResult bindingResult) throws ExecutionException, InterruptedException { + if(bindingResult != null && bindingResult.hasErrors()){ + return BaseResponseUtils.buildFail(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage()); + } + + String a = null; + try { + a = futureValue.get(10, TimeUnit.SECONDS); + } catch (TimeoutException e) { + return BaseResponseUtils.buildFail("1鍒嗛挓鍚庡幓鏌ョ湅缁撴灉"); + } + futureValue = new CompletableFuture<>(); + + Map<String, Object> param = new HashMap<>(); + param.put("controllerType", "01"); + param.put("projectNo", 100); + param.put("rtuNewAddr", "202405061656120001"); + + Map<String, Object> postParams = new HashMap<>(); + postParams.put("id", 2024050616450001L); + postParams.put("protocol", "p1"); + postParams.put("rtuAddr", "20001"); + postParams.put("type", "outerCommand"); + postParams.put("code", "10"); + postParams.put("noRtMwDealRes", false); + postParams.put("rtuResultSendWebUrl", "127.0.0.1/remote/"); + postParams.put("param", param); + + Map<String, String> headerParams = new HashMap<>(); + + JSONObject job_result = restTemplateUtils.post("http://localhost:8070/accMw/com/send", JSON.toJSONString(postParams), headerParams); + + return BaseResponseUtils.buildSuccess(a) ; + } + + @GetMapping("/setValue") + public String setValue(String name) { + futureValue.complete(name); + return "Value set"; + } +} diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/dto/DTOValve.java b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/dto/DTOValve.java new file mode 100644 index 0000000..0f4f645 --- /dev/null +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/java/com/dy/pipIrrRemote/valve/dto/DTOValve.java @@ -0,0 +1,48 @@ +package com.dy.pipIrrRemote.valve.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Range; + +/** + * @author ZhuBaoMin + * @date 2024-05-07 15:05 + * @LastEditTime 2024-05-07 15:05 + * @Description 杩滅▼寮�闃�銆佽繙绋嬪叧闃�浼犲叆瀵硅薄 + */ + +@Data +@Schema(name = "寮�鍏抽榾浼犲叆瀵硅薄") +public class DTOValve { + public static final long serialVersionUID = 202405071506001L; + + /** + * 鍙栨按鍙D + */ + @Schema(description = "鍙栨按鍙D", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @NotNull(message = "鍙栨按鍙d笉鑳戒负绌�") + private Long intakeId; + + /** + * 铏氭嫙鍗D + */ + @Schema(description = "铏氭嫙鍗D", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @NotNull(message = "铏氭嫙鍗′笉鑳戒负绌�") + private Long vcId; + + /** + * 鎿嶄綔绫诲瀷锛�1-寮�闃�锛�2-鍏抽榾 + */ + @Schema(description = "鎿嶄綔绫诲瀷", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @NotNull(message = "鎿嶄綔绫诲瀷涓嶈兘涓虹┖") + @Range(min = 1, max = 2) + private Integer operateType; + + /** + * 鎿嶄綔浜� + */ + @Schema(description = "鎿嶄綔浜�", requiredMode = Schema.RequiredMode.NOT_REQUIRED) + @NotNull(message = "鎿嶄綔浜轰笉鑳戒负绌�") + private Long operator; +} diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/resources/application.yml b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/resources/application.yml index 9627a3d..a3e69d7 100644 --- a/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/resources/application.yml +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-remote/src/main/resources/application.yml @@ -6,7 +6,7 @@ management: server: port: ${pipIrr.remote.actutorPort} -#web鏈嶅姟绔彛锛宼omcat榛樿鏄�8080 +#web鏈嶅姟绔彛锛宼omcat榛樿鏄�8081 server: port: ${pipIrr.remote.webPort} servlet: diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/PipIrrSellApplication.java b/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/PipIrrSellApplication.java index f8b120d..5f7f0a7 100644 --- a/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/PipIrrSellApplication.java +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/PipIrrSellApplication.java @@ -28,6 +28,4 @@ } - - } diff --git a/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/config/RestTemplateConfig.java b/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/config/RestTemplateConfig.java index cb8956b..58cefe3 100644 --- a/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/config/RestTemplateConfig.java +++ b/pipIrr-platform/pipIrr-web/pipIrr-web-sell/src/main/java/com/dy/pipIrrSell/config/RestTemplateConfig.java @@ -42,4 +42,10 @@ return restTemplate; } + + //绠�鍗昍estTemplate瀹炰緥 + @Bean + public RestTemplate simpleRestTemplate() { + return new RestTemplate(); + } } \ No newline at end of file -- Gitblit v1.8.0