aboutsummaryrefslogtreecommitdiffstats
path: root/mso-api-handlers/mso-api-handler-infra/src/main/java
diff options
context:
space:
mode:
authorSeshu Kumar M <seshu.kumar.m@huawei.com>2017-09-19 11:44:53 +0000
committerGerrit Code Review <gerrit@onap.org>2017-09-19 11:44:53 +0000
commite493976735b14cb5e946c3ae058bbe26871b944c (patch)
tree5e77435b159b1e293aee65e4e8f56f126172f6e1 /mso-api-handlers/mso-api-handler-infra/src/main/java
parentc7e6f2b9ab81bc1ca6721d68a8f5369dedfefb28 (diff)
parentad4e77a9898f56e2d3045275331d1ee00baa20f2 (diff)
Merge "Add Rest for VoLTE service IssueId: SO-60"
Diffstat (limited to 'mso-api-handlers/mso-api-handler-infra/src/main/java')
-rw-r--r--mso-api-handlers/mso-api-handler-infra/src/main/java/org/openecomp/mso/apihandlerinfra/E2EServiceInstances.java481
1 files changed, 481 insertions, 0 deletions
diff --git a/mso-api-handlers/mso-api-handler-infra/src/main/java/org/openecomp/mso/apihandlerinfra/E2EServiceInstances.java b/mso-api-handlers/mso-api-handler-infra/src/main/java/org/openecomp/mso/apihandlerinfra/E2EServiceInstances.java
new file mode 100644
index 0000000000..46a7d43acc
--- /dev/null
+++ b/mso-api-handlers/mso-api-handler-infra/src/main/java/org/openecomp/mso/apihandlerinfra/E2EServiceInstances.java
@@ -0,0 +1,481 @@
+/*-
+ * ============LICENSE_START=======================================================
+ * ONAP - SO
+ * ================================================================================
+ * Copyright (C) 2017 Huawei Technologies Co., Ltd. All rights reserved.
+ * ================================================================================
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ============LICENSE_END=========================================================
+ */
+
+package org.openecomp.mso.apihandlerinfra;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.codehaus.jackson.map.ObjectMapper;
+import org.openecomp.mso.apihandler.common.ErrorNumbers;
+import org.openecomp.mso.apihandler.common.RequestClient;
+import org.openecomp.mso.apihandler.common.RequestClientFactory;
+import org.openecomp.mso.apihandler.common.ResponseHandler;
+import org.openecomp.mso.apihandlerinfra.e2eserviceinstancebeans.E2EServiceInstanceRequest;
+import org.openecomp.mso.apihandlerinfra.e2eserviceinstancebeans.E2EUserParam;
+import org.openecomp.mso.apihandlerinfra.serviceinstancebeans.ServiceInstancesRequest;
+import org.openecomp.mso.db.catalog.CatalogDatabase;
+import org.openecomp.mso.db.catalog.beans.Service;
+import org.openecomp.mso.db.catalog.beans.ServiceRecipe;
+import org.openecomp.mso.logger.MessageEnum;
+import org.openecomp.mso.logger.MsoAlarmLogger;
+import org.openecomp.mso.logger.MsoLogger;
+import org.openecomp.mso.requestsdb.InfraActiveRequests;
+import org.openecomp.mso.requestsdb.RequestsDatabase;
+import org.openecomp.mso.utils.UUIDChecker;
+
+@Path("/")
+public class E2EServiceInstances {
+
+ private HashMap<String, String> instanceIdMap = new HashMap<String,String>();
+ private static MsoLogger msoLogger = MsoLogger.getMsoLogger (MsoLogger.Catalog.APIH);
+ private static MsoAlarmLogger alarmLogger = new MsoAlarmLogger ();
+ public final static String MSO_PROP_APIHANDLER_INFRA = "MSO_PROP_APIHANDLER_INFRA";
+
+ public E2EServiceInstances() {
+ }
+
+ @POST
+ @Path("e2eServiceInstances/{version:[vV][2-3]}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response createE2EServiceInstance(String request, @PathParam("version") String version) {
+
+ Response response = E2EserviceInstances(request, Action.createInstance, null, version);
+
+ return response;
+ }
+
+ @DELETE
+ @Path("e2eServiceInstances/{version:[vV][2-3]}/{serviceId}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response deleteE2EServiceInstance(String request, @PathParam("version") String version) {
+
+ Response response = E2EserviceInstances(request, Action.deleteInstance, null, version);
+
+ return response;
+ }
+
+ private Response E2EserviceInstances(String requestJSON, Action action,
+ HashMap<String, String> instanceIdMap, String version) {
+
+ String requestId = UUIDChecker.generateUUID(msoLogger);
+ long startTime = System.currentTimeMillis();
+ msoLogger.debug("requestId is: " + requestId);
+ E2EServiceInstanceRequest sir = null;
+
+ MsoRequest msoRequest = new MsoRequest(requestId);
+ ObjectMapper mapper = new ObjectMapper();
+ try {
+ sir = mapper
+ .readValue(requestJSON, E2EServiceInstanceRequest.class);
+
+ } catch (Exception e) {
+
+ msoLogger.debug("Mapping of request to JSON object failed : ", e);
+ Response response = msoRequest.buildServiceErrorResponse(
+ HttpStatus.SC_BAD_REQUEST,
+ MsoException.ServiceException,
+ "Mapping of request to JSON object failed. "
+ + e.getMessage(), ErrorNumbers.SVC_BAD_PARAMETER,
+ null);
+ msoLogger.error(MessageEnum.APIH_REQUEST_VALIDATION_ERROR,
+ MSO_PROP_APIHANDLER_INFRA, "", "",
+ MsoLogger.ErrorCode.SchemaError, requestJSON, e);
+ msoLogger.recordAuditEvent(startTime, MsoLogger.StatusCode.ERROR,
+ MsoLogger.ResponseCode.SchemaError,
+ "Mapping of request to JSON object failed");
+ msoLogger.debug("End of the transaction, the final response is: "
+ + (String) response.getEntity());
+ return response;
+ }
+
+ InfraActiveRequests dup = null;
+ String instanceName = sir.getService().getName();
+ String requestScope = sir.getService().getParameters().getNodeType();
+
+ try {
+ if(!(instanceName==null && requestScope.equals("service") && (action == Action.createInstance || action == Action.activateInstance))){
+ dup = (RequestsDatabase.getInstance()).checkInstanceNameDuplicate (instanceIdMap, instanceName, requestScope);
+ }
+ } catch (Exception e) {
+ msoLogger.error (MessageEnum.APIH_DUPLICATE_CHECK_EXC, MSO_PROP_APIHANDLER_INFRA, "", "", MsoLogger.ErrorCode.DataError, "Error during duplicate check ", e);
+
+ Response response = msoRequest.buildServiceErrorResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, MsoException.ServiceException,
+ e.getMessage(),
+ ErrorNumbers.SVC_DETAILED_SERVICE_ERROR,
+ null) ;
+
+
+ msoLogger.recordAuditEvent (startTime, MsoLogger.StatusCode.ERROR, MsoLogger.ResponseCode.DBAccessError, "Error during duplicate check");
+ msoLogger.debug ("End of the transaction, the final response is: " + (String) response.getEntity ());
+ return response;
+ }
+
+ if (dup != null) {
+ // Found the duplicate record. Return the appropriate error.
+ String instance = null;
+ if(instanceName != null){
+ instance = instanceName;
+ }else{
+ instance = instanceIdMap.get(requestScope + "InstanceId");
+ }
+ String dupMessage = "Error: Locked instance - This " + requestScope + " (" + instance + ") " + "already has a request being worked with a status of " + dup.getRequestStatus() + " (RequestId - " + dup.getRequestId() + "). The existing request must finish or be cleaned up before proceeding.";
+
+ Response response = msoRequest.buildServiceErrorResponse(HttpStatus.SC_CONFLICT, MsoException.ServiceException,
+ dupMessage,
+ ErrorNumbers.SVC_DETAILED_SERVICE_ERROR,
+ null) ;
+
+
+ msoLogger.warn (MessageEnum.APIH_DUPLICATE_FOUND, dupMessage, "", "", MsoLogger.ErrorCode.SchemaError, "Duplicate request - Subscriber already has a request for this service");
+ msoRequest.createRequestRecord (Status.FAILED, action);
+ msoLogger.recordAuditEvent (startTime, MsoLogger.StatusCode.ERROR, MsoLogger.ResponseCode.Conflict, dupMessage);
+ msoLogger.debug ("End of the transaction, the final response is: " + (String) response.getEntity ());
+ return response;
+ }
+
+ CatalogDatabase db = null;
+ try {
+ db = CatalogDatabase.getInstance();
+ } catch (Exception e) {
+ msoLogger.error (MessageEnum.APIH_DB_ACCESS_EXC, MSO_PROP_APIHANDLER_INFRA, "", "", MsoLogger.ErrorCode.AvailabilityError, "Exception while communciate with Catalog DB", e);
+ msoRequest.setStatus (org.openecomp.mso.apihandlerinfra.vnfbeans.RequestStatusType.FAILED);
+ Response response = msoRequest.buildServiceErrorResponse (HttpStatus.SC_NOT_FOUND,
+ MsoException.ServiceException,
+ "No communication to catalog DB " + e.getMessage (),
+ ErrorNumbers.SVC_NO_SERVER_RESOURCES,
+ null);
+ alarmLogger.sendAlarm ("MsoDatabaseAccessError",
+ MsoAlarmLogger.CRITICAL,
+ Messages.errors.get (ErrorNumbers.NO_COMMUNICATION_TO_CATALOG_DB));
+ msoRequest.createRequestRecord (Status.FAILED,action);
+ msoLogger.recordAuditEvent (startTime, MsoLogger.StatusCode.ERROR, MsoLogger.ResponseCode.DBAccessError, "Exception while communciate with DB");
+ msoLogger.debug ("End of the transaction, the final response is: " + (String) response.getEntity ());
+ return response;
+ }
+
+
+ RecipeLookupResult recipeLookupResult = null;
+
+ try {
+ recipeLookupResult = getServiceInstanceOrchestrationURI(db, sir, action);
+ } catch (Exception e) {
+ msoLogger.error (MessageEnum.APIH_DB_ACCESS_EXC, MSO_PROP_APIHANDLER_INFRA, "", "", MsoLogger.ErrorCode.DataError, "Exception while querying Catalog DB", e);
+ msoRequest.setStatus (org.openecomp.mso.apihandlerinfra.vnfbeans.RequestStatusType.FAILED);
+ Response response = msoRequest.buildServiceErrorResponse (HttpStatus.SC_NOT_FOUND,
+ MsoException.ServiceException,
+ "Recipe could not be retrieved from catalog DB " + e.getMessage (),
+ ErrorNumbers.SVC_GENERAL_SERVICE_ERROR,
+ null);
+ alarmLogger.sendAlarm ("MsoDatabaseAccessError",
+ MsoAlarmLogger.CRITICAL,
+ Messages.errors.get (ErrorNumbers.ERROR_FROM_CATALOG_DB));
+ msoRequest.createRequestRecord (Status.FAILED,action);
+ msoLogger.recordAuditEvent (startTime, MsoLogger.StatusCode.ERROR, MsoLogger.ResponseCode.DBAccessError, "Exception while querying Catalog DB");
+ msoLogger.debug ("End of the transaction, the final response is: " + (String) response.getEntity ());
+ db.close();
+ return response;
+ }
+ finally{
+ if(null != db)
+ {
+ db.close();
+ }
+
+ }
+
+ if (recipeLookupResult == null) {
+ msoLogger.error (MessageEnum.APIH_DB_ATTRIBUTE_NOT_FOUND, MSO_PROP_APIHANDLER_INFRA, "", "", MsoLogger.ErrorCode.DataError, "No recipe found in DB");
+ msoRequest.setStatus (org.openecomp.mso.apihandlerinfra.vnfbeans.RequestStatusType.FAILED);
+ Response response = msoRequest.buildServiceErrorResponse (HttpStatus.SC_NOT_FOUND,
+ MsoException.ServiceException,
+ "Recipe does not exist in catalog DB",
+ ErrorNumbers.SVC_GENERAL_SERVICE_ERROR,
+ null);
+ msoRequest.createRequestRecord (Status.FAILED, action);
+ msoLogger.recordAuditEvent (startTime, MsoLogger.StatusCode.ERROR, MsoLogger.ResponseCode.DataNotFound, "No recipe found in DB");
+ msoLogger.debug ("End of the transaction, the final response is: " + (String) response.getEntity ());
+
+ return response;
+ }
+
+
+ String modelInfo = sir.getService().getParameters().getNodeTemplateName();
+ String[] arrayOfInfo = modelInfo.split(":");
+ String serviceInstanceType = arrayOfInfo[0];
+
+
+
+ String serviceInstanceId = "";
+
+ RequestClient requestClient = null;
+ HttpResponse response = null;
+
+ long subStartTime = System.currentTimeMillis();
+ String sirRequestJson = mappingObtainedRequestJSONToServiceInstanceRequest(sir);
+
+ try {
+ requestClient = RequestClientFactory.getRequestClient (recipeLookupResult.getOrchestrationURI (), MsoPropertiesUtils.loadMsoProperties ());
+
+ // Capture audit event
+ msoLogger.debug ("MSO API Handler Posting call to BPEL engine for url: " + requestClient.getUrl ());
+
+ response = requestClient.post(requestId, false,
+ recipeLookupResult.getRecipeTimeout(),
+ action.name(), serviceInstanceId, null, null, null, null, serviceInstanceType,
+ null, null, null, sirRequestJson);
+
+ msoLogger.recordMetricEvent(subStartTime,
+ MsoLogger.StatusCode.COMPLETE, MsoLogger.ResponseCode.Suc,
+ "Successfully received response from BPMN engine", "BPMN",
+ recipeLookupResult.getOrchestrationURI(), null);
+ } catch (Exception e) {
+ msoLogger.recordMetricEvent(subStartTime,
+ MsoLogger.StatusCode.ERROR,
+ MsoLogger.ResponseCode.CommunicationError,
+ "Exception while communicate with BPMN engine", "BPMN",
+ recipeLookupResult.getOrchestrationURI(), null);
+ Response resp = msoRequest.buildServiceErrorResponse(
+ HttpStatus.SC_BAD_GATEWAY, MsoException.ServiceException,
+ "Failed calling bpmn " + e.getMessage(),
+ ErrorNumbers.SVC_NO_SERVER_RESOURCES, null);
+ alarmLogger.sendAlarm("MsoConfigurationError",
+ MsoAlarmLogger.CRITICAL,
+ Messages.errors.get(ErrorNumbers.NO_COMMUNICATION_TO_BPEL));
+ msoLogger.error(MessageEnum.APIH_BPEL_COMMUNICATE_ERROR,
+ MSO_PROP_APIHANDLER_INFRA, "", "",
+ MsoLogger.ErrorCode.AvailabilityError,
+ "Exception while communicate with BPMN engine");
+ msoLogger.recordAuditEvent(startTime, MsoLogger.StatusCode.ERROR,
+ MsoLogger.ResponseCode.CommunicationError,
+ "Exception while communicate with BPMN engine");
+ msoLogger.debug("End of the transaction, the final response is: "
+ + (String) resp.getEntity());
+ return resp;
+ }
+
+ if (response == null) {
+ Response resp = msoRequest.buildServiceErrorResponse(
+ HttpStatus.SC_BAD_GATEWAY, MsoException.ServiceException,
+ "bpelResponse is null",
+ ErrorNumbers.SVC_NO_SERVER_RESOURCES, null);
+ msoLogger.error(MessageEnum.APIH_BPEL_COMMUNICATE_ERROR,
+ MSO_PROP_APIHANDLER_INFRA, "", "",
+ MsoLogger.ErrorCode.BusinessProcesssError,
+ "Null response from BPEL");
+ msoLogger.recordAuditEvent(startTime, MsoLogger.StatusCode.ERROR,
+ MsoLogger.ResponseCode.InternalError,
+ "Null response from BPMN");
+ msoLogger.debug("End of the transaction, the final response is: "
+ + (String) resp.getEntity());
+ return resp;
+ }
+
+ ResponseHandler respHandler = new ResponseHandler(response,
+ requestClient.getType());
+ int bpelStatus = respHandler.getStatus();
+
+ // BPEL accepted the request, the request is in progress
+ if (bpelStatus == HttpStatus.SC_ACCEPTED) {
+ String camundaJSONResponseBody = respHandler.getResponseBody();
+ msoLogger
+ .debug("Received from Camunda: " + camundaJSONResponseBody);
+ (RequestsDatabase.getInstance()).updateInfraStatus(requestId,
+ Status.IN_PROGRESS.toString(),
+ Constants.PROGRESS_REQUEST_IN_PROGRESS,
+ Constants.MODIFIED_BY_APIHANDLER);
+
+ msoLogger.recordAuditEvent(startTime,
+ MsoLogger.StatusCode.COMPLETE, MsoLogger.ResponseCode.Suc,
+ "BPMN accepted the request, the request is in progress");
+ msoLogger.debug("End of the transaction, the final response is: "
+ + (String) camundaJSONResponseBody);
+ return Response.status(HttpStatus.SC_ACCEPTED)
+ .entity(camundaJSONResponseBody).build();
+ } else {
+ List<String> variables = new ArrayList<String>();
+ variables.add(bpelStatus + "");
+ String camundaJSONResponseBody = respHandler.getResponseBody();
+ if (camundaJSONResponseBody != null
+ && !camundaJSONResponseBody.isEmpty()) {
+ Response resp = msoRequest.buildServiceErrorResponse(
+ bpelStatus, MsoException.ServiceException,
+ "Request Failed due to BPEL error with HTTP Status= %1 "
+ + '\n' + camundaJSONResponseBody,
+ ErrorNumbers.SVC_DETAILED_SERVICE_ERROR, variables);
+ msoLogger.error(MessageEnum.APIH_BPEL_RESPONSE_ERROR,
+ requestClient.getUrl(), "", "",
+ MsoLogger.ErrorCode.BusinessProcesssError,
+ "Response from BPEL engine is failed with HTTP Status="
+ + bpelStatus);
+ msoLogger.recordAuditEvent(startTime,
+ MsoLogger.StatusCode.ERROR,
+ MsoLogger.ResponseCode.InternalError,
+ "Response from BPMN engine is failed");
+ msoLogger
+ .debug("End of the transaction, the final response is: "
+ + (String) resp.getEntity());
+ return resp;
+ } else {
+ Response resp = msoRequest
+ .buildServiceErrorResponse(
+ bpelStatus,
+ MsoException.ServiceException,
+ "Request Failed due to BPEL error with HTTP Status= %1",
+ ErrorNumbers.SVC_DETAILED_SERVICE_ERROR,
+ variables);
+ msoLogger.error(MessageEnum.APIH_BPEL_RESPONSE_ERROR,
+ requestClient.getUrl(), "", "",
+ MsoLogger.ErrorCode.BusinessProcesssError,
+ "Response from BPEL engine is empty");
+ msoLogger.recordAuditEvent(startTime,
+ MsoLogger.StatusCode.ERROR,
+ MsoLogger.ResponseCode.InternalError,
+ "Response from BPEL engine is empty");
+ msoLogger
+ .debug("End of the transaction, the final response is: "
+ + (String) resp.getEntity());
+ return resp;
+ }
+ }
+ }
+
+ private RecipeLookupResult getServiceInstanceOrchestrationURI(
+ CatalogDatabase db, E2EServiceInstanceRequest sir, Action action) {
+
+ RecipeLookupResult recipeLookupResult = null;
+
+ recipeLookupResult = getServiceURI(db, sir, action);
+
+ if (recipeLookupResult != null) {
+ msoLogger.debug ("Orchestration URI is: " + recipeLookupResult.getOrchestrationURI() + ", recipe Timeout is: " + Integer.toString(recipeLookupResult.getRecipeTimeout ()));
+ }
+ else {
+ msoLogger.debug("No matching recipe record found");
+ }
+ return recipeLookupResult;
+ }
+
+ private RecipeLookupResult getServiceURI(CatalogDatabase db,
+ E2EServiceInstanceRequest sir, Action action) {
+
+ String defaultServiceModelName = "UUI_DEFAULT";
+
+ Service serviceRecord = null;
+ ServiceRecipe recipe = null;
+
+ serviceRecord = db.getServiceByModelName(defaultServiceModelName);
+ recipe = db.getServiceRecipeByModelUUID(serviceRecord.getModelUUID(), action.name());
+
+ if (recipe == null) {
+ return null;
+ }
+ return new RecipeLookupResult(recipe.getOrchestrationUri(),
+ recipe.getRecipeTimeout());
+
+ }
+
+ private String mappingObtainedRequestJSONToServiceInstanceRequest(E2EServiceInstanceRequest e2eSir){
+
+ ServiceInstancesRequest sir = new ServiceInstancesRequest();
+
+ String returnString = null;
+
+ //ModelInvariantId
+ sir.getRequestDetails().getModelInfo().setModelInvariantId(e2eSir.getService().getServiceDefId());
+
+ //modelNameVersionId
+ sir.getRequestDetails().getModelInfo().setModelNameVersionId(e2eSir.getService().getTemplateId());
+
+ String modelInfo = e2eSir.getService().getParameters().getNodeTemplateName();
+ String[] arrayOfInfo = modelInfo.split(":");
+ String modelName = arrayOfInfo[0];
+ String modelVersion = arrayOfInfo[0];
+
+ //modelName
+ sir.getRequestDetails().getModelInfo().setModelName(modelName);
+
+ //modelVersion
+ sir.getRequestDetails().getModelInfo().setModelVersion(modelVersion);
+
+ //modelType
+ if(ModelType.service.equals(e2eSir.getService().getParameters().getNodeType())){
+ sir.getRequestDetails().getModelInfo().setModelType(ModelType.service);
+ }
+
+ sir.getRequestDetails().getModelInfo().getModelType();
+
+ //globalsubscriberId
+ sir.getRequestDetails().getSubscriberInfo().setGlobalSubscriberId(e2eSir.getService().getParameters().getGlobalSubscriberId());
+
+ //subscriberName
+ sir.getRequestDetails().getSubscriberInfo().setSubscriberName(e2eSir.getService().getParameters().getSubscriberName());
+
+ //instanceName
+ sir.getRequestDetails().getRequestInfo().setInstanceName(e2eSir.getService().getName());
+
+ //source
+ sir.getRequestDetails().getRequestInfo().setSource("UUI");
+
+ //suppressRollback
+ sir.getRequestDetails().getRequestInfo().setSuppressRollback(true);
+
+ //subscriptionServiceType
+ sir.getRequestDetails().getRequestParameters().setSubscriptionServiceType("MOG");
+
+ //Userparams
+ List<E2EUserParam> userParams = new ArrayList<>();
+ userParams = e2eSir.getService().getParameters().getRequestParameters().getUserParams();
+ List<Map<String, String>> userParamList = new ArrayList<Map<String,String>>();
+ Map<String,String> userParamMap= new HashMap<String, String>();
+ for(E2EUserParam userp: userParams){
+ userParamMap.put(userp.getName(), userp.getValue());
+ userParamList.add(userParamMap);
+ }
+
+ sir.getRequestDetails().getRequestParameters().setUserParams(userParamList);
+
+ //converting to string
+ ObjectMapper mapper = new ObjectMapper();
+ try {
+ returnString = mapper.writeValueAsString(sir);
+ } catch (IOException e) {
+ msoLogger.debug("Exception while converting ServiceInstancesRequest object to string", e);
+ }
+
+ return returnString;
+ }
+} \ No newline at end of file